From 5fa31cf103b4c4e29de279a501ba4b48fd584674 Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Thu, 20 Apr 2023 13:12:48 +0200 Subject: [PATCH 001/295] Initial commit --- .dockerignore | 3 + .editorconfig | 17 + .gitattributes | 413 +++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 51 ++ .github/ISSUE_TEMPLATE/epic_template.md | 60 +++ .github/ISSUE_TEMPLATE/feature_request.md | 39 ++ .github/PULL_REQUEST_TEMPLATE.md | 43 ++ .github/dependabot.yml | 52 +++ .github/license_scan_config.yml | 13 + .github/workflows/add_issue_to_project.yml | 16 + .../workflows/add_pullrequest_to_project.yml | 16 + .github/workflows/code_analysis.yml | 59 +++ .github/workflows/license_scan.yml | 23 + .github/workflows/secret_scan.yml | 25 + .github/workflows/security_scan.yml | 30 ++ .gitignore | 434 ++++++++++++++++++ .pre-commit-README.md | 25 + .pre-commit-config.yaml | 7 + CHANGELOG.md | 19 + CODE_OF_CONDUCT.md | 70 +++ CONTRIBUTING.md | 162 +++++++ LICENSE.md | 0 README.md | 293 ++++++++++++ SECURITY.md | 32 ++ STYLEGUIDE.md | 58 +++ resources/checkstyle-config.xml | 430 +++++++++++++++++ 26 files changed, 2390 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/epic_template.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/license_scan_config.yml create mode 100644 .github/workflows/add_issue_to_project.yml create mode 100644 .github/workflows/add_pullrequest_to_project.yml create mode 100644 .github/workflows/code_analysis.yml create mode 100644 .github/workflows/license_scan.yml create mode 100644 .github/workflows/secret_scan.yml create mode 100644 .github/workflows/security_scan.yml create mode 100644 .gitignore create mode 100644 .pre-commit-README.md create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 STYLEGUIDE.md create mode 100644 resources/checkstyle-config.xml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..c81b8d319 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +.env diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..1da5f17fe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..067d366da --- /dev/null +++ b/.gitattributes @@ -0,0 +1,413 @@ +# This tells Git to enforce Unix-style line endings across all clones of the repository, preventing various issues that might arise from developing on Windows for Linux. https://help.github.com/articles/dealing-with-line-endings/ +* text=auto eol=lf + +# Created with https://gitattributes.io +# Edit at https://gitattributes.io/api/web%2Cjava%2Cc%2B%2B%2Ccsharp%2Ccommon%2Cgo%2Cvisualstudio + +# Sources +*.c text diff=c +*.cc text diff=cpp +*.cxx text diff=cpp +*.cpp text diff=cpp +*.c++ text diff=cpp +*.hpp text diff=cpp +*.h text diff=c +*.h++ text diff=cpp +*.hh text diff=cpp + +# Compiled Object files +*.slo binary +*.lo binary +*.o binary +*.obj binary + +# Precompiled Headers +*.gch binary +*.pch binary + +# Compiled Dynamic libraries +*.so binary +*.dylib binary +*.dll binary + +# Compiled Static libraries +*.lai binary +*.la binary +*.a binary +*.lib binary + +# Executables +*.exe binary +*.out binary +*.app binary +# Common settings that generally should always be used with your language specific settings + +# Auto detect text files and perform LF normalization +# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ +* text=auto + +# +# The above will handle all files NOT found below +# + +# Documents +*.bibtex text diff=bibtex +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.md text +*.tex text diff=tex +*.adoc text +*.textile text +*.mustache text +*.csv text +*.tab text +*.tsv text +*.txt text +*.sql text + +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +# SVG treated as an asset (binary) by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary +*.eps binary + +# Scripts +*.bash text eol=lf +*.fish text eol=lf +*.sh text eol=lf +# These are explicitly windows files and should use crlf +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Serialisation +*.json text +*.toml text +*.xml text +*.yaml text +*.yml text + +# Archives +*.7z binary +*.gz binary +*.tar binary +*.tgz binary +*.zip binary + +# Text files where line endings should be preserved +*.patch -text + +# +# Exclude files from exporting +# + +.gitattributes export-ignore +.gitignore export-ignore +# Auto detect text files and perform LF normalization +# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ +* text=auto + +*.cs text diff=csharp + +# Go sources +*.go text diff=golang +*.mod text eol=lf +*.sum text eol=lf + +# Java sources +*.java text diff=java +*.gradle text diff=java +*.gradle.kts text diff=java + +# These files are text and should be normalized (Convert crlf => lf) +*.css text diff=css +*.df text +*.htm text diff=html +*.html text diff=html +*.js text +*.jsp text +*.jspf text +*.jspx text +*.properties text +*.tld text +*.tag text +*.tagx text +*.xml text + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.class binary +*.dll binary +*.ear binary +*.jar binary +*.so binary +*.war binary +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just comment the entries below and +# uncomment the group further below +############################################################################### + +*.sln text eol=crlf +*.csproj text eol=crlf +*.vbproj text eol=crlf +*.vcxproj text eol=crlf +*.vcproj text eol=crlf +*.dbproj text eol=crlf +*.fsproj text eol=crlf +*.lsproj text eol=crlf +*.wixproj text eol=crlf +*.modelproj text eol=crlf +*.sqlproj text eol=crlf +*.wmaproj text eol=crlf + +*.xproj text eol=crlf +*.props text eol=crlf +*.filters text eol=crlf +*.vcxitems text eol=crlf + + +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +#*.xproj merge=binary +#*.props merge=binary +#*.filters merge=binary +#*.vcxitems merge=binary +## GITATTRIBUTES FOR WEB PROJECTS +# +# These settings are for any web project. +# +# Details per file setting: +# text These files should be normalized (i.e. convert CRLF to LF). +# binary These files are binary and should be left untouched. +# +# Note that binary is a macro for -text -diff. +###################################################################### + +# Auto detect +## Handle line endings automatically for files detected as +## text and leave all files detected as binary untouched. +## This will handle all files NOT defined below. +* text=auto + +# Source code +*.bash text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +*.coffee text +*.css text +*.htm text diff=html +*.html text diff=html +*.inc text +*.ini text +*.js text +*.json text +*.jsx text +*.less text +*.ls text +*.map text -diff +*.od text +*.onlydata text +*.php text diff=php +*.pl text +*.ps1 text eol=crlf +*.py text diff=python +*.rb text diff=ruby +*.sass text +*.scm text +*.scss text diff=css +*.sh text eol=lf +*.sql text +*.styl text +*.tag text +*.ts text +*.tsx text +*.xml text +*.xhtml text diff=html + +# Docker +Dockerfile text + +# Documentation +*.ipynb text +*.markdown text +*.md text +*.mdwn text +*.mdown text +*.mkd text +*.mkdn text +*.mdtxt text +*.mdtext text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +# Templates +*.dot text +*.ejs text +*.haml text +*.handlebars text +*.hbs text +*.hbt text +*.jade text +*.latte text +*.mustache text +*.njk text +*.phtml text +*.tmpl text +*.tpl text +*.twig text +*.vue text + +# Configs +*.cnf text +*.conf text +*.config text +.editorconfig text +.env text +.gitattributes text +.gitconfig text +.htaccess text +*.lock text -diff +package-lock.json text -diff +*.toml text +*.yaml text +*.yml text +browserslist text +Makefile text +makefile text + +# Heroku +Procfile text + +# Graphics +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.gifv binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +# SVG treated as an asset (binary) by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + +# Audio +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +# Video +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.ogv binary +*.swc binary +*.swf binary +*.webm binary + +# Archives +*.7z binary +*.gz binary +*.jar binary +*.rar binary +*.tar binary +*.zip binary + +# Fonts +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +# Executables +*.exe binary +*.pyc binary + +# RC files (like .babelrc or .eslintrc) +*.*rc text + +# Ignore files (like .npmignore or .gitignore) +*.*ignore text diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..4f82e18c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,51 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "" +labels: "kind/bug" +assignees: "" +--- + +# Bug Report + +## Description + +_A clear and concise description of the bug._ +_If applicable, add screenshots or other information to help explain your problem._ + +### Expected Behavior + +_A clear and concise description of what you expected to happen._ + +### Observed Behavior + +_A clear and concise description of what happened instead._ + +## Steps to Reproduce + +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Context Information + +_Add any other context about the problem here._ + +- Environment: Which instance or deployment were you using when the error occurred? Staging, Productive, Connector-URL, etc.? +- Used version: If a specific component was used, which Version did you use? E.g. EDC v1.0.0 +- Time: When did the error occur? Exact timestamp (incl. time zone; assumed CET/CEST if missing) if possible. +- Logs: Error log or further information +- Parameter: Request contents or information entered +- OS: [e.g. iOS, Windows] +- ... + +## Possible Implementation and Work Breakdown + +_You already know the root cause of the erroneous state and how to fix it? Feel free to share your thoughts._ + +- [ ] Task 1 +- [ ] Task 2 +- ... diff --git a/.github/ISSUE_TEMPLATE/epic_template.md b/.github/ISSUE_TEMPLATE/epic_template.md new file mode 100644 index 000000000..605f78b4d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic_template.md @@ -0,0 +1,60 @@ +--- +name: Epic +about: Help us with new ideas +title: "" +labels: "kind/epic" +assignees: "" +--- + +# Epic + +## Description + +_Brief summary of what this Epic is, whether it's a larger project, goal, or user story. Describe the job to be done, which persona this Epic is mainly for, or if more multiple, break it down by user and job story._ + +### Requirements + +_Which requirements do you have to be fulfilled?_ + +- Requirement 1 +- Requirement 2 + +## Work Breakdown + +_Create Stories which can be converted into issues_ + +- [ ] Story 1 +- [ ] Story 2 +- ... + +## Initiative / goal + +_Describe how this Epic impacts an initiative the business is working on._ + +### Hypothesis + +_What is your hypothesis on the success of this Epic? Describe how success will be measured and what leading indicators the team will have to know if success has been hit._ + +## Acceptance criteria and must have scope + +_Define what is a must-have for launch and in-scope. Keep this section fluid and dynamic until you lock-in priority during planning._ + +- Criteria 1 +- Criteria 2 +- ... + +## Stakeholders + +_Describe who needs to be kept up-to-date about this Epic, included in discussions, or updated along the way. Stakeholders can be both in Product/Engineering, as well as other teams like Customer Success who might want to keep customers updated on the epic project._ + +## Timeline + +_What's the timeline for this Epic, what resources are needed, and what might potentially block this from hitting the projected end date._ + +## Need for refinement + +_Which questions are open? From whom do you need more input to fully specify the epic?_ + +- Question 1 +- Question 2 +- ... diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..ab525302d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,39 @@ +--- +name: Feature Request +about: Help us with new features +title: "" +labels: "kind/enhancement" +assignees: "" +--- + +# Feature Request + +## Description + +_A clear and concise description of what the customer wants to happen._ + +- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. + +## Which Areas Would Be Affected? + +_e.g., DPF, CI, build, transfer, etc._ + +## Why Is the Feature Desired? + +_Are there any requirements?_ + +## How does this tie into our current product? + +_Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new markets and innovative ideas?_ + +## Stakeholders + +_Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ + +## Solution Proposal and Work Breakdown + +_If possible, provide a (brief!) solution proposal._ + +- [ ] Step 1 +- [ ] Step 2 +- ... diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..fb93cb530 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ +# Pull Request + +_Briefly describe WHAT your PR changes, which features it adds/modifies._ + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- Test A +- Test B +- ... + +**Test Configuration**: + +- Firmware version: +- Hardware: +- Toolchain: +- SDK: + +## Linked Issue(s) + +_Use keywords to automate: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword_ + +- fixes # (issue) +- closes # (issue) +- ... + +## PR is blocked by + +- [ ] blocked by # (issue) + +# Checklist + +- [ ] I have **formatted the title** correctly and precisely +- [ ] My code follows the **style guidelines** of this project +- [ ] I have performed a **self-review** of my own code +- [ ] I have **commented** my code, particularly in hard-to-understand areas and public classes/methods +- [ ] I have made corresponding changes to the **documentation** +- [ ] My changes generate **no new warnings** (performed checkstyle check locally) +- [ ] I have added **tests that prove my fix** is effective or that my feature works +- [ ] New and existing unit **tests pass locally** with my changes +- [ ] Any dependent **changes have been merged** and published in downstream modules +- [ ] I have added/updated **copyright headers** diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5a0d3f3f9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,52 @@ +version: 2 +updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "main" + open-pull-requests-limit: 30 + labels: + - "area/dependency" + - "area/github" + # Docker + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + target-branch: "main" + open-pull-requests-limit: 30 + labels: + - "area/dependency" + - "area/docker" + # Gradle + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + target-branch: "main" + open-pull-requests-limit: 30 + labels: + - "area/dependency" + - "area/java" + # Maven + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + target-branch: "main" + open-pull-requests-limit: 30 + labels: + - "area/dependency" + - "area/java" + # NPM + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + target-branch: "main" + open-pull-requests-limit: 30 + labels: + - "area/dependency" + - "area/javascript" diff --git a/.github/license_scan_config.yml b/.github/license_scan_config.yml new file mode 100644 index 000000000..bf7c41855 --- /dev/null +++ b/.github/license_scan_config.yml @@ -0,0 +1,13 @@ +format: table +vulnerability: + type: + - os + - library + ignore-unfixed: true +scan: + security-checks: + - license +license-full: true +severity: + - HIGH + - CRITICAL diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml new file mode 100644 index 000000000..bbecd2d2a --- /dev/null +++ b/.github/workflows/add_issue_to_project.yml @@ -0,0 +1,16 @@ +name: Add issue to project action + +on: + issues: + types: + - opened + +jobs: + add_issue_to_project: + name: add_issue_to_project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/sovity/projects/9 + github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} diff --git a/.github/workflows/add_pullrequest_to_project.yml b/.github/workflows/add_pullrequest_to_project.yml new file mode 100644 index 000000000..9ca64829c --- /dev/null +++ b/.github/workflows/add_pullrequest_to_project.yml @@ -0,0 +1,16 @@ +name: Add pull request to project action + +on: + pull_request: + +jobs: + add_pullrequest_to_project: + name: add_pullrequest_to_project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/sovity/projects/9 + github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} + labeled: area/dependency + label-operator: NOT diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml new file mode 100644 index 000000000..0e9cab29b --- /dev/null +++ b/.github/workflows/code_analysis.yml @@ -0,0 +1,59 @@ +name: Code Analysis + +on: + workflow_dispatch: + pull_request: + branches: [main] + paths-ignore: + - "**.md" + - "docs/**" + +jobs: + is_java_project: + runs-on: ubuntu-latest + outputs: + pom_exists: ${{ steps.check_files.outputs.files_exists }} + checkstyle_active: ${{ steps.check_checkstyle.outputs.checkstyle_active }} + spotbugs_active: ${{ steps.check_spotbugs.outputs.spotbugs_active }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Check file existence + id: check_files + uses: andstor/file-existence-action@v2 + with: + files: "pom.xml" + - name: check_checkstyle + id: check_checkstyle + run: echo "checkstyle_active=$(if grep -q "maven-checkstyle-plugin" pom.xml; then echo "true"; else echo "false"; fi)" >> $GITHUB_OUTPUT + - name: check_spotbugs + id: check_spotbugs + run: echo "spotbugs_active=$(if grep -q "spotbugs-maven-plugin" pom.xml; then echo "true"; else echo "false"; fi)" >> $GITHUB_OUTPUT + run_checkstyle: + needs: [is_java_project] + if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.checkstyle_active == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + cache: "maven" + - name: Run style checks + run: mvn -B checkstyle:check --file pom.xml + run_spotbugs: + needs: [is_java_project] + if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.spotbugs_active == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + cache: "maven" + - name: Run static code analysis + run: mvn -B compile spotbugs:check --file pom.xml diff --git a/.github/workflows/license_scan.yml b/.github/workflows/license_scan.yml new file mode 100644 index 000000000..fb9a2ded7 --- /dev/null +++ b/.github/workflows/license_scan.yml @@ -0,0 +1,23 @@ +name: Trivy License Scan + +on: + pull_request: + branches: ["main"] + +jobs: + license_scan: + name: license_scan + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run license scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "rootfs" + scan-ref: "." + scanners: "license" + severity: "CRITICAL,HIGH" + exit-code: 1 diff --git a/.github/workflows/secret_scan.yml b/.github/workflows/secret_scan.yml new file mode 100644 index 000000000..b27e1f6b1 --- /dev/null +++ b/.github/workflows/secret_scan.yml @@ -0,0 +1,25 @@ +name: Trivy Secret Scan + +on: + push: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + secret-scan: + name: secret_scan + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Run vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + exit-code: "1" + ignore-unfixed: true + scanners: secret diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml new file mode 100644 index 000000000..c43bd288b --- /dev/null +++ b/.github/workflows/security_scan.yml @@ -0,0 +1,30 @@ +name: Trivy Security Scan + +on: + pull_request: + branches: ["main"] + +jobs: + security_scan: + name: security_scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run static analysis + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + scanners: "vuln,config" + ignore-unfixed: true + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + continue-on-error: true + with: + sarif_file: "trivy-results.sarif" + category: "code" diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..48a56f90d --- /dev/null +++ b/.gitignore @@ -0,0 +1,434 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,node,intellij+all,intellij,windows,linux,maven,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=java,node,intellij+all,intellij,windows,linux,maven,gradle + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Node ### +# Logs +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +# JDT-specific (Eclipse Java Development Tools) + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/java,node,intellij+all,intellij,windows,linux,maven,gradle + +/.idea/.gitignore +/.idea/checkstyle-idea.xml +/.idea/github-templates.iml +/.idea/sonarlint/issuestore/index.pb +/.idea/jpa-buddy.xml +/.idea/misc.xml +/.idea/modules.xml +/.idea/vcs.xml diff --git a/.pre-commit-README.md b/.pre-commit-README.md new file mode 100644 index 000000000..eae74228d --- /dev/null +++ b/.pre-commit-README.md @@ -0,0 +1,25 @@ +# Pre-Commit-Hook +The defined pre-commit-hook prevents committing passwords to the repository. In case a password is detected +git commit fails. + +## Install pre-commit and detect-secrets +1. Install pre-commit-hook tool + `$ pip install pre-commit` +2. Install detect-secrets + `$ pip install detect-secrets` + +## Enable secret-scanning pre-commit hook +1. Update pre-commit-hook + `$ pre-commit autoupdate` +2. Enable defined pre-commit-hook + `$ pre-commit install` + +## On repository initialization of pre-commit hook with detect-secrets +If no `.secrets.baseline` is present, simply generate it: +1. `$ detect-secrets scan --disable-plugin KeywordDetector --disable-plugin AWSKeyDetector > .secrets.baseline` +2. Use Notepad++ or IntelliJ-Editor to convert `.secrets.baseline` to UTF-8 + +## Add false-positives or force adding secrets +1. `$ detect-secrets scan --baseline .secrets.baseline` +2. If secrets are identified, add them to .secrets.baseline manually +For more details see: https://github.com/Yelp/detect-secrets#adding-secrets-to-baseline diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..5d62bf09d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + exclude: package.lock.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..984623b36 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] - yyyy-mm-dd + +### Added + +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Security diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..dc748a7ef --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,70 @@ +# Code of Conduct +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), +version 1.4, available at http://contributor-covenant.org/version/1/4. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..cf973be5a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,162 @@ +# Contributing to the Project + +Thank you for your interest in contributing to this project + +## Table of Contents + +* [Code Of Conduct](#code-of-conduct) +* [How to Contribute](#how-to-contribute) + * [Discuss](#discuss) + * [Create an Issue](#create-an-issue) + * [Submit a Pull Request](#submit-a-pull-request) + * [Report on Flaky Tests](#report-on-flaky-tests) +* [Etiquette for pull requests](#etiquette-for-pull-requests) +* [Contact Us](#contact-us) + +## Code Of Conduct + +See the [Code Of Conduct](CODE_OF_CONDUCT.md). + +## How to Contribute + +### Discuss + +If you want to share an idea to further enhance the project or discuss potential use cases, please feel free to create a +discussion at the `GitHub Discussions page`] +If you feel there is a bug or an issue, contribute to the discussions in `existing issues` +otherwise [create a new issue](#create-an-issue). + +### Create an Issue + +If you have identified a bug or want to formulate a working item that you want to concentrate on, feel free to create a +new issue at our project's corresponding `GitHub Issues page`. Before doing so, please consider searching for +potentially suitable `existing issues`. + +We also +use [GitHub's default label set](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) +extended by custom ones to classify issues and improve findability. + +If an issue appears to cover changes that will have a (huge) impact on the code base and needs to +first be discussed, or if you just have a question regarding the usage of the software, please +create a `discussion` before raising an issue. + +Please note that if an issue covers a topic or the response to a question that may be interesting +for other developers or contributors, or for further discussions, it should be converted to a +discussion and not be closed. + +### Adhere to Coding Style Guide + +We aim for a coherent and consistent code base, thus the coding style detailed in the [styleguide](STYLEGUIDE.md) should +be followed. + +### Submit a Pull Request + +We would appreciate if your pull request applies to the following points: + +* Conform to following [Etiquette for pull requests](#etiquette-for-pull-requests): + +* Make sure to adjust copyright headers appropriately. + +* The git commit messages should comply to the following format: + ``` + (): + ``` + + Use the [imperative mood](https://github.com/git/git/blob/master/Documentation/SubmittingPatches) + as in "Fix bug" or "Add feature" rather than "Fixed bug" or "Added feature" and + [mention the GitHub issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) + e.g. `chore(transfer process): improve logging`. + +* Add meaningful tests to verify your submission acts as expected. + +* Where code is not self-explanatory, add documentation providing extra clarification. + +* PR descriptions should use the current [PR template](.github/PULL_REQUEST_TEMPLATE.md) + +* Submit a draft pull request at early-stage and add people previously working on the same code as + reviewer. Make sure automatic checks pass before marking it as "ready for review": + + * _Continuous Integration_ performing various test conventions. + +### Report on Flaky Tests + +If you discover a randomly failing ("flaky") test, please take the time to check whether an issue for that already +exists and if not, create an issue yourself, providing meaningful description and a link to the failing run. Please also +label it with `Bug` and `github`. Then assign it to whoever was the original author of the relevant piece of code or +whoever worked on it last. If assigning the issue is not possible due to missing rights, please just comment and +@mention the author/last editor. + +Please do not just restart the run, as this would overwrite the results. If you need to, a better way of doing this is +to push an empty commit. This will trigger another run. + +```bash +git commit --allow-empty -m "trigger CI" && git push +``` + +If an issue labeled with `Bug` and `github` is assigned to you, please prioritize addressing this issue as other +people will be affected. +We are taking the quality of our code very serious and reporting on flaky tests is an important step toward improvement +in that area. + +## Etiquette for pull requests + +### As an author + +Submitting pull requests should be done while adhering to a couple of simple rules. + +- Familiarize yourself with [coding style](STYLEGUIDE.md), architectural patterns and other contribution guidelines. +- No surprise PRs please. Before you submit a PR, open a discussion or an issue outlining your planned work and give + people time to comment. It may even be advisable to contact committers using the `@mention` feature. Unsolicited PRs + may get ignored or rejected. +- Create focused PRs: your work should be focused on one particular feature or bug. Do not create broad-scoped PRs that + solve multiple issues as reviewers may reject those PR bombs outright. +- Provide a clear description and motivation in the PR description in GitHub. This makes the reviewer's life much + easier. It is also helpful to outline the broad changes that were made, e.g. "Changes the schema of XYZ-Entity: + the `age` field changed from `long` to `String`". +- If you introduce new 3rd party dependencies, be sure to note them in the PR description and explain why they are + necessary. +- Stick to the established code style, please refer to the [styleguide document](STYLEGUIDE.md). +- All tests should be green, especially when your PR is in `"Ready for review"` +- Mark PRs as `"Ready for review"` only when you're prepared to defend your work. By that time you have completed your + work and shouldn't need to push any more commits other than to incorporate review comments. +- Merge conflicts should be resolved by squashing all commits on the PR branch, rebasing onto `main` and + force-pushing. Do this when your PR is ready to review. +- If you require a reviewer's input while it's still in draft, please contact the designated reviewer using + the `@mention` feature and let them know what you'd like them to look at. +- Re-request reviews after all remarks have been adopted. This helps reviewers track their work in GitHub. +- If you disagree with a committer's remarks, feel free to object and argue, but if no agreement is reached, you'll have + to either accept the decision or withdraw your PR. +- Be civil and objective. No foul language, insulting or otherwise abusive language will be tolerated. +- The PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). + - The title must follow the format as `(): `. + `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test` are allowed for + the ``. + - The length must be kept under 80 characters. + +### As a reviewer + +- Have a look at [Pull Request Review Pyramide](https://www.morling.dev/blog/the-code-review-pyramid/) +- Please complete reviews within two business days or delegate to another committer, removing yourself as a reviewer. +- If you have been requested as reviewer, but cannot do the review for any reason (time, lack of knowledge in particular + area, etc.) please comment that in the PR and remove yourself as a reviewer, suggesting a stand-in. +- Don't be overly pedantic. +- Don't argue basic principles (code style, architectural decisions, etc.) +- Use the `suggestion` feature of GitHub for small/simple changes. +- The following could serve you as a review checklist: + - no unnecessary dependencies in `build.gradle.kts` + - sensible unit tests, prefer unit tests over integration tests wherever possible (test runtime). Also check the + usage of test tags. + - code style + - simplicity and "uncluttered-ness" of the code + - overall focus of the PR +- Don't just wave through any PR. Please take the time to look at them carefully. +- Be civil and objective. No foul language, insulting or otherwise abusive language will be tolerated. The goal is to + _encourage_ contributions. + +## Contact Us + +If you have questions or suggestions, do not hesitate to contact the project developers via https://github.com/sovity. + +## Attribution + +This file is adapted from the [eclipse-edc](https://github.com/eclipse-dataspaceconnector/DataSpaceConnector) project. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e69de29bb diff --git a/README.md b/README.md new file mode 100644 index 000000000..7e1f9dc97 --- /dev/null +++ b/README.md @@ -0,0 +1,293 @@ +# About this Template Repository + +## Secret Scan Action + +The action scans with trivy for secrets +Files: + +- .github/workflows/secret_scan.yml + +## Pre-commit Hook + +Please see readme +Files: + +- .pre-commit-config.yaml +- .pre-commit-README.md + +## Readme Template Preamble + +Here's a blank template to get started: To avoid retyping too much info. Do a search and replace with your text editor +for the +following: `github-templates`, `twitter_handle`, `linkedin_username`, `email_client`, `email`, `project_title`, `project_description` +
+ +# Readme + + + + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![MIT License][license-shield]][license-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + + +
+
+ + Logo + + +

project_title

+ +

+ project_description +
+ Explore the docs » +
+
+ View Demo + · + Report Bug + · + Request Feature +

+
+ + + + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Roadmap
  8. +
  9. Contributing
  10. +
  11. License
  12. +
  13. Contact
  14. +
  15. Acknowledgments
  16. +
+
+ + + + + +## About The Project + +[![Product Name Screen Shot][product-screenshot]](https://example.com) + +

(back to top)

+ +### Built With + +* [![Next][Next.js]][Next-url] +* [![React][React.js]][React-url] +* [![Vue][Vue.js]][Vue-url] +* [![Angular][Angular.io]][Angular-url] +* [![Svelte][Svelte.dev]][Svelte-url] +* [![Laravel][Laravel.com]][Laravel-url] +* [![Bootstrap][Bootstrap.com]][Bootstrap-url] +* [![JQuery][JQuery.com]][JQuery-url] + +

(back to top)

+ + + + + +## Getting Started + +This is an example of how you may give instructions on setting up your project locally. +To get a local copy up and running follow these simple example steps. + +### Prerequisites + +This is an example of how to list things you need to use the software and how to install them. + +* npm + ```sh + npm install npm@latest -g + ``` + +### Installation + +1. Get a free API Key at [https://example.com](https://example.com) +2. Clone the repo + ```sh + git clone https://github.com/sovity/github-templates.git + ``` +3. Install NPM packages + ```sh + npm install + ``` +4. Enter your API in `config.js` + ```js + const API_KEY = 'ENTER YOUR API'; + ``` + +

(back to top)

+ + + + + +## Usage + +Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos +work well in this space. You may also link to more resources. + +_For more examples, please refer to the [Documentation](https://example.com)_ + +

(back to top)

+ + + + + +## Roadmap + +- [ ] Feature 1 +- [ ] Feature 2 +- [ ] Feature 3 + - [ ] Nested Feature + +See the [open issues](https://github.com/sovity/github-templates/issues) for a full list of proposed features (and known +issues). + +

(back to top)

+ + + + + +## Contributing + +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any +contributions you make are **greatly appreciated**. + +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also +simply open an issue with the tag "enhancement". +Don't forget to give the project a star! Thanks again! + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +

(back to top)

+ + + + + +## License + +Distributed under the MIT License. See `LICENSE.txt` for more information. + +

(back to top)

+ + + + + +## Contact + +Your Name - [@twitter_handle](https://twitter.com/twitter_handle) - email@email_client.com + +Project Link: [https://github.com/sovity/github-templates](https://github.com/sovity/github-templates) + +

(back to top)

+ + + + + +## Acknowledgments + +* []() +* []() +* []() + +

(back to top)

+ + + + + + +[contributors-shield]: https://img.shields.io/github/contributors/sovity/github-templates.svg?style=for-the-badge + +[contributors-url]: https://github.com/sovity/github-templates/graphs/contributors + +[forks-shield]: https://img.shields.io/github/forks/sovity/github-templates.svg?style=for-the-badge + +[forks-url]: https://github.com/sovity/github-templates/network/members + +[stars-shield]: https://img.shields.io/github/stars/sovity/github-templates.svg?style=for-the-badge + +[stars-url]: https://github.com/sovity/github-templates/stargazers + +[issues-shield]: https://img.shields.io/github/issues/sovity/github-templates.svg?style=for-the-badge + +[issues-url]: https://github.com/sovity/github-templates/issues + +[license-shield]: https://img.shields.io/github/license/sovity/github-templates.svg?style=for-the-badge + +[license-url]: https://github.com/sovity/github-templates/blob/master/LICENSE.txt + +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 + +[linkedin-url]: https://linkedin.com/in/linkedin_username + +[product-screenshot]: images/screenshot.png + +[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white + +[Next-url]: https://nextjs.org/ + +[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB + +[React-url]: https://reactjs.org/ + +[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D + +[Vue-url]: https://vuejs.org/ + +[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white + +[Angular-url]: https://angular.io/ + +[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 + +[Svelte-url]: https://svelte.dev/ + +[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white + +[Laravel-url]: https://laravel.com + +[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white + +[Bootstrap-url]: https://getbootstrap.com + +[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white + +[JQuery-url]: https://jquery.com diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..b412ae0ef --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ +## Security + +sovity GmbH takes the security of its software products and services seriously, which includes all source code repositories managed through our GitHub organization: [sovity](https://github.com/sovity). + +If you believe you have found a security vulnerability in any of sovity's owned repositories, please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them via mail: [security@sovity.de](mailto:security@sovity.de) + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages + +We prefer all communications to be in English. + +## Attribution +This file is adapted from [eclipse-edc](https://github.com/eclipse-edc/DataDashboard) project. diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md new file mode 100644 index 000000000..400c7edcb --- /dev/null +++ b/STYLEGUIDE.md @@ -0,0 +1,58 @@ +# Code Style Guide + +In order to maintain a coherent code style throughout the project we ask every contributor to adhere to a few simple +style guidelines. We assume most developers will use at least something like `IntelliJ` and therefore have support for +automatic code formatting, we are not going to list the guidelines here. If you absolutely want to take a look, checkout +the [config written in XML](resources/checkstyle-config.xml). + +## Checkstyle configuration + +Checkstyle is a [tool](https://checkstyle.sourceforge.io/) that can statically analyze your source code to check against +a set of given rules. Those rules are formulated in an [XML document](resources/checkstyle-config.xml). Many modern +IDEs have a plugin available for download that runs in the background and does code analysis. + +This checkstyle config is based off of the [Google Style](https://checkstyle.sourceforge.io/google_style.html) with a +few +additional rules such as the naming of constants and Types. + +_Note: currently we do **not** enforce the generation of Javadoc comments, even though documenting code is **highly** +recommended. We might enable this in the future, such that at least interfaces and public methods are commented._ + +## Running Checkstyle + +In order to get better usability and on-the-fly reporting, Checkstyle is available as IDE plugins for many modern IDEs, +and it can run either on-demand or continuously in the background: + +- [IntelliJ IDEA plugin [recommended]](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) +- [Eclipse IDE [recommended]](https://checkstyle.org/eclipse-cs/#!/) + +### Checkstyle as PR validation + +Apart from running Checkstyle locally as IDE plugin, we do run it on +our [Github Actions pipeline](.github/workflows/code_analysis.yml). At this time, Checkstyle will only spew out warnings, but +we may tighten the rules at a future time and without notice. This will result in failing Github Action pipelines. Also, +committers might reject PRs due to Checkstyle warnings. + +It is therefore **highly** recommended running Checkstyle locally as well. + +If you **do not wish** to run Checkstyle on you local machine, that's fine, but be prepared to get your PRs rejected +simply because of a stupid naming or formatting error. + +> _Note: we do not use the Checkstyle Gradle Plugin on Github Actions because violations would cause builds to fail. For +now, we only want to log warnings._ + +## [Recommended] IntelliJ Code Style Configuration + +If you are using Jetbrains IntelliJ IDEA, we have created a specific code style configuration that will automatically +format your source code according to that style guide. This should eliminate most of the potential Checkstyle violations +right from the get-go. You will need to reformat your code manually or in a pre-commit hook though. + +## [Optional] Generic `.editorconfig` + +For most other editors and IDEs we've supplied an [.editorconfig](.editorconfig) file that can be +placed at the appropriate location. The specific location will largely depend on your editor and your OS, please refer +to the [official documentation](https://editorconfig.org) for details. + +## Attribution + +This file is adapted from the [eclipse-edc](https://github.com/eclipse-dataspaceconnector/DataSpaceConnector) project. diff --git a/resources/checkstyle-config.xml b/resources/checkstyle-config.xml new file mode 100644 index 000000000..a5d01d00f --- /dev/null +++ b/resources/checkstyle-config.xml @@ -0,0 +1,430 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 9cf4c0cd540842c1b2d47774336bdc24e05a1475 Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Thu, 20 Apr 2023 16:45:22 +0200 Subject: [PATCH 002/295] Update add_issue_to_project.yml --- .github/workflows/add_issue_to_project.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index bbecd2d2a..d2cbf15db 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -14,3 +14,7 @@ jobs: with: project-url: https://github.com/orgs/sovity/projects/9 github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/sovity/projects/21 + github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} From bbc8389a3cc7d6fd9bab7f3e7bc67aaa09378ea7 Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Thu, 20 Apr 2023 16:45:41 +0200 Subject: [PATCH 003/295] Update add_pullrequest_to_project.yml --- .github/workflows/add_pullrequest_to_project.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/add_pullrequest_to_project.yml b/.github/workflows/add_pullrequest_to_project.yml index 9ca64829c..f80f2d5db 100644 --- a/.github/workflows/add_pullrequest_to_project.yml +++ b/.github/workflows/add_pullrequest_to_project.yml @@ -14,3 +14,9 @@ jobs: github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} labeled: area/dependency label-operator: NOT + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/sovity/projects/21 + github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} + labeled: area/dependency + label-operator: NOT From ba7f866c09b50fac62c68477aebae9dc35e33abc Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Fri, 28 Apr 2023 15:14:18 +0200 Subject: [PATCH 004/295] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4f82e18c9..c61ca1a46 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug Report about: Create a report to help us improve title: "" -labels: "kind/bug" +labels: ["kind/bug", "scope/mds"] assignees: "" --- From 6b4698e8905867fe3cd8c41d67074c522159f948 Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Fri, 28 Apr 2023 15:14:29 +0200 Subject: [PATCH 005/295] Update epic_template.md --- .github/ISSUE_TEMPLATE/epic_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/epic_template.md b/.github/ISSUE_TEMPLATE/epic_template.md index 605f78b4d..961c29ada 100644 --- a/.github/ISSUE_TEMPLATE/epic_template.md +++ b/.github/ISSUE_TEMPLATE/epic_template.md @@ -2,7 +2,7 @@ name: Epic about: Help us with new ideas title: "" -labels: "kind/epic" +labels: ["kind/epic", "scope/mds"] assignees: "" --- From 6f66c9548981075e195f01afdb8f626f6f05c174 Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Fri, 28 Apr 2023 15:14:40 +0200 Subject: [PATCH 006/295] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ab525302d..ba9d3218f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature Request about: Help us with new features title: "" -labels: "kind/enhancement" +labels: ["kind/enhancement", "scope/mds"] assignees: "" --- From 353c34a6b6a33401ed368a7c46b534eae5ec04ea Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 10 May 2023 13:00:16 +0200 Subject: [PATCH 007/295] chore: initial broker server extension, cleaned up project (#21) * chore: init project * chore: init project * chore clean up initial setup * chore clean up initial setup * chore clean up initial setup * chore clean up initial setup * chore clean up initial setup * chore clean up initial setup * chore: init project * remove IAIS maven repo * add missing github secret --------- Co-authored-by: Richard Treier --- .../build-and-publish-connector-images.yml | 80 +++++ CHANGELOG.md | 12 +- LICENSE.md | 201 ++++++++++++ README.md | 288 +++--------------- build.gradle.kts | 79 +++++ connector/.env | 102 +++++++ connector/Dockerfile | 43 +++ connector/README.md | 43 +++ connector/build.gradle.kts | 46 +++ .../src/main/resources/logging.properties | 7 + docker-compose.yaml | 61 ++++ .../dev/checkstyle}/checkstyle-config.xml | 26 +- extensions/broker-server/README.md | 38 +++ extensions/broker-server/build.gradle.kts | 45 +++ .../brokerserver/BrokerServerExtension.java | 46 +++ .../BrokerServerExtensionContext.java | 26 ++ .../BrokerServerExtensionContextBuilder.java | 36 +++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + gradle.properties | 24 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 ++++++++++++++ gradlew.bat | 89 ++++++ settings.gradle.kts | 4 + 24 files changed, 1279 insertions(+), 257 deletions(-) create mode 100644 .github/workflows/build-and-publish-connector-images.yml create mode 100644 build.gradle.kts create mode 100644 connector/.env create mode 100644 connector/Dockerfile create mode 100644 connector/README.md create mode 100644 connector/build.gradle.kts create mode 100644 connector/src/main/resources/logging.properties create mode 100644 docker-compose.yaml rename {resources => docs/dev/checkstyle}/checkstyle-config.xml (94%) create mode 100644 extensions/broker-server/README.md create mode 100644 extensions/broker-server/build.gradle.kts create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java create mode 100644 extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.github/workflows/build-and-publish-connector-images.yml b/.github/workflows/build-and-publish-connector-images.yml new file mode 100644 index 000000000..38d2a6739 --- /dev/null +++ b/.github/workflows/build-and-publish-connector-images.yml @@ -0,0 +1,80 @@ +name: EDC Image CI + +on: + push: + branches: [ main ] + release: + types: [ published ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME_BASE: ${{ github.repository_owner }} + IMAGE_NAME: edc + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + strategy: + matrix: + imageVariants: [ + { + "imageName": "broker-server-dev", + "title": "Broker Server (Dev)", + "description": "EDC IDS Broker Server. This dev version contains no persistence or auth and can be used to quickly start a locally running Broker Server + Broker UI.", + "buildArgs": "-Pdmgmt-api-key" + }, + { + "imageName": "broker-server-ce", + "title": "Broker Server (Community Edition)", + "description": "EDC IDS Broker Server. Contains DB extensions and requires dataspace credentials to join an existing dataspace.", + "buildArgs": "-Pfs-vault -Pdmgmt-api-key -Ppostgres-flyway -Poauth2" + } + ] + timeout-minutes: 30 + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BASE }}/${{ matrix.imageVariants.imageName }} + labels: | + org.opencontainers.image.title=${{ matrix.imageVariants.title }} + org.opencontainers.image.description=${{ matrix.imageVariants.description }} + tags: | + type=schedule + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=release,enable=${{ startsWith(github.ref, 'refs/tags/') }} + - name: Build and push EDC image + uses: docker/build-push-action@v4 + with: + file: connector/Dockerfile + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + USERNAME=${{ github.actor }} + TOKEN=${{ secrets.GITHUB_TOKEN }} + BUILD_ARGS=${{ matrix.imageVariants.buildArgs }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 984623b36..f2aa42e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - yyyy-mm-dd -### Added +### Major -### Changed +### Minor -### Deprecated - -### Removed - -### Fixed - -### Security +### Patch diff --git a/LICENSE.md b/LICENSE.md index e69de29bb..46a734a70 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2022 sovity.de + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 7e1f9dc97..972f2ee8c 100644 --- a/README.md +++ b/README.md @@ -1,293 +1,105 @@ -# About this Template Repository - -## Secret Scan Action - -The action scans with trivy for secrets -Files: - -- .github/workflows/secret_scan.yml - -## Pre-commit Hook - -Please see readme -Files: - -- .pre-commit-config.yaml -- .pre-commit-README.md - -## Readme Template Preamble - -Here's a blank template to get started: To avoid retyping too much info. Do a search and replace with your text editor -for the -following: `github-templates`, `twitter_handle`, `linkedin_username`, `email_client`, `email`, `project_title`, `project_description` -
- -# Readme - + + [![Contributors][contributors-shield]][contributors-url] -[![Forks][forks-shield]][forks-url] -[![Stargazers][stars-shield]][stars-url] +[![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] -[![MIT License][license-shield]][license-url] +[![Apache 2.0][license-shield]][license-url] [![LinkedIn][linkedin-shield]][linkedin-url]
- - Logo - - -

project_title

- -

- project_description -
- Explore the docs » -
-
- View Demo - · - Report Bug - · - Request Feature -

-
- + +Logo + +

Broker Server

+

+Broker Backend & EDC Extensions. +
+Report Bug +· +Request Feature +

+
- Table of Contents -
    -
  1. - About The Project - -
  2. -
  3. - Getting Started - -
  4. -
  5. Usage
  6. -
  7. Roadmap
  8. -
  9. Contributing
  10. -
  11. License
  12. -
  13. Contact
  14. -
  15. Acknowledgments
  16. -
+ Table of Contents +
    +
  1. About The Project
  2. +
  3. License
  4. +
  5. Contact
  6. +
- - ## About The Project -[![Product Name Screen Shot][product-screenshot]](https://example.com) - -

(back to top)

- -### Built With - -* [![Next][Next.js]][Next-url] -* [![React][React.js]][React-url] -* [![Vue][Vue.js]][Vue-url] -* [![Angular][Angular.io]][Angular-url] -* [![Svelte][Svelte.dev]][Svelte-url] -* [![Laravel][Laravel.com]][Laravel-url] -* [![Bootstrap][Bootstrap.com]][Bootstrap-url] -* [![JQuery][JQuery.com]][JQuery-url] +[Eclipse Dataspace Components](https://github.com/eclipse-edc) (EDC) is a framework +for building dataspaces, exchanging data securely with ensured data sovereignty. -

(back to top)

- - - - - -## Getting Started - -This is an example of how you may give instructions on setting up your project locally. -To get a local copy up and running follow these simple example steps. - -### Prerequisites - -This is an example of how to list things you need to use the software and how to install them. - -* npm - ```sh - npm install npm@latest -g - ``` - -### Installation - -1. Get a free API Key at [https://example.com](https://example.com) -2. Clone the repo - ```sh - git clone https://github.com/sovity/github-templates.git - ``` -3. Install NPM packages - ```sh - npm install - ``` -4. Enter your API in `config.js` - ```js - const API_KEY = 'ENTER YOUR API'; - ``` +[sovity](https://sovity.de/) extends the EDC Connector's functionality with extensions to offer +enterprise-ready managed services like "Connector-as-a-Service", out-of-the-box fully configured DAPS +and integrations to existing other dataspace technologies. -

(back to top)

- - - - - -## Usage - -Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos -work well in this space. You may also link to more resources. +An IDS Broker is a central component of a dataspace that operates on the IDS protocol, that aggregates and indexes +connectors and data offers. -_For more examples, please refer to the [Documentation](https://example.com)_ +This IDS Broker is written on basis of the EDC and should be used in tandem with the Broker UI.

(back to top)

- - - - -## Roadmap - -- [ ] Feature 1 -- [ ] Feature 2 -- [ ] Feature 3 - - [ ] Nested Feature - -See the [open issues](https://github.com/sovity/github-templates/issues) for a full list of proposed features (and known -issues). - -

(back to top)

- - - - - -## Contributing - -Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any -contributions you make are **greatly appreciated**. - -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also -simply open an issue with the tag "enhancement". -Don't forget to give the project a star! Thanks again! - -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - -

(back to top)

- - - ## License -Distributed under the MIT License. See `LICENSE.txt` for more information. +Distributed under the Apache 2.0 License. See `LICENSE` for more information.

(back to top)

- - ## Contact -Your Name - [@twitter_handle](https://twitter.com/twitter_handle) - email@email_client.com - -Project Link: [https://github.com/sovity/github-templates](https://github.com/sovity/github-templates) +contact@sovity.de

(back to top)

- - - - -## Acknowledgments - -* []() -* []() -* []() - -

(back to top)

- - - -[contributors-shield]: https://img.shields.io/github/contributors/sovity/github-templates.svg?style=for-the-badge - -[contributors-url]: https://github.com/sovity/github-templates/graphs/contributors - -[forks-shield]: https://img.shields.io/github/forks/sovity/github-templates.svg?style=for-the-badge - -[forks-url]: https://github.com/sovity/github-templates/network/members - -[stars-shield]: https://img.shields.io/github/stars/sovity/github-templates.svg?style=for-the-badge - -[stars-url]: https://github.com/sovity/github-templates/stargazers - -[issues-shield]: https://img.shields.io/github/issues/sovity/github-templates.svg?style=for-the-badge - -[issues-url]: https://github.com/sovity/github-templates/issues - -[license-shield]: https://img.shields.io/github/license/sovity/github-templates.svg?style=for-the-badge - -[license-url]: https://github.com/sovity/github-templates/blob/master/LICENSE.txt - -[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 - -[linkedin-url]: https://linkedin.com/in/linkedin_username - -[product-screenshot]: images/screenshot.png - -[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white - -[Next-url]: https://nextjs.org/ - -[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB - -[React-url]: https://reactjs.org/ - -[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D +[contributors-shield]: +https://img.shields.io/github/contributors/sovity/edc-broker-server-extension.svg?style=for-the-badge -[Vue-url]: https://vuejs.org/ +[contributors-url]: https://github.com/sovity/edc-broker-server-extension/graphs/contributors -[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white +[forks-shield]: +https://img.shields.io/github/forks/sovity/edc-broker-server-extension.svg?style=for-the-badge -[Angular-url]: https://angular.io/ +[forks-url]: https://github.com/sovity/edc-broker-server-extension/network/members -[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 +[stars-shield]: +https://img.shields.io/github/stars/sovity/edc-broker-server-extension.svg?style=for-the-badge -[Svelte-url]: https://svelte.dev/ +[stars-url]: https://github.com/sovity/edc-broker-server-extension/stargazers -[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white +[issues-shield]: +https://img.shields.io/github/issues/sovity/edc-broker-server-extension.svg?style=for-the-badge -[Laravel-url]: https://laravel.com +[issues-url]: https://github.com/sovity/edc-broker-server-extension/issues -[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white +[license-shield]: +https://img.shields.io/github/license/sovity/edc-broker-server-extension.svg?style=for-the-badge -[Bootstrap-url]: https://getbootstrap.com +[license-url]: https://github.com/sovity/edc-broker-server-extension/blob/master/LICENSE.txt -[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white +[linkedin-shield]: +https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 -[JQuery-url]: https://jquery.com +[linkedin-url]: https://www.linkedin.com/company/sovity diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..4cae5a540 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,79 @@ +plugins { + id("java") + id("checkstyle") + id("maven-publish") +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") +} + +val downloadArtifact: Configuration by configurations.creating { + isTransitive = false +} + +allprojects { + apply(plugin = "java") + apply(plugin = "checkstyle") + + tasks.withType { + options.encoding = "UTF-8" + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() + } + + tasks.getByName("test") { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + } + + checkstyle { + toolVersion = "9.0" + configFile = rootProject.file("docs/dev/checkstyle/checkstyle-config.xml") + configDirectory.set(rootProject.file("docs/dev/checkstyle")) + maxErrors = 0 // does not tolerate errors + } + + repositories { + mavenCentral() + mavenLocal() + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/sovity/edc-extensions") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + } +} + +subprojects { + apply(plugin = "maven-publish") + + val sovityBrokerServerGroup: String by project + val sovityBrokerServerVersion: String by project + + + group = sovityBrokerServerGroup + version = sovityBrokerServerVersion + + publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/sovity/edc-broker-server-extension") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + } + } +} diff --git a/connector/.env b/connector/.env new file mode 100644 index 000000000..77545aa7b --- /dev/null +++ b/connector/.env @@ -0,0 +1,102 @@ +# Default ENV Vars + +# This file will be sourced as bash script: +# - KEY=Value will become KEY=${KEY:-"Value"}, so that ENV Vars can be overwritten by parent docker-compose.yaml. +# - Watch out for escaping issues as values will be surrounded by quotes, and dollar signs must be escaped. + +# Deployment Settings +MY_EDC_FQDN=missing-env-MY_EDC_FQDN +MY_EDC_BASE_PATH= +MY_EDC_PROTOCOL=https:// + +# MY_EDC_NAME_KEBAB_CASE +EDC_CONNECTOR_NAME=$MY_EDC_NAME_KEBAB_CASE +EDC_IDS_ID=urn:connector:$MY_EDC_NAME_KEBAB_CASE + +# MY_EDC_TITLE +EDC_IDS_TITLE=$MY_EDC_TITLE + +# MY_EDC_DESCRIPTION +EDC_IDS_DESCRIPTION=$MY_EDC_DESCRIPTION + +# MY_EDC_CURATOR_URL +EDC_IDS_CURATOR=$MY_EDC_CURATOR_URL + +# MY_EDC_CURATOR_NAME +EDC_UI_CURATOR_ORGANIZATION_NAME=$MY_EDC_CURATOR_NAME + +# MY_EDC_MAINTAINER_URL +EDC_IDS_MAINTAINER=$MY_EDC_MAINTAINER_URL + +# MY_EDC_MAINTAINER_NAME +EDC_UI_MAINTAINER_ORGANIZATION_NAME=$MY_EDC_MAINTAINER_NAME + +# Ports and Paths +WEB_HTTP_PORT=11001 +WEB_HTTP_MANAGEMENT_PORT=11002 +WEB_HTTP_PROTOCOL_PORT=11003 +WEB_HTTP_CONTROL_PORT=11004 +WEB_HTTP_PATH=${MY_EDC_BASE_PATH}/api +WEB_HTTP_MANAGEMENT_PATH=${MY_EDC_BASE_PATH}/api/v1/management +WEB_HTTP_PROTOCOL_PATH=${MY_EDC_BASE_PATH}/api/v1/ids +WEB_HTTP_CONTROL_PATH=${MY_EDC_BASE_PATH}/api/v1/control + + +EDC_HOSTNAME=${MY_EDC_FQDN} +EDC_UI_CONNECTOR_ID=${MY_EDC_PROTOCOL}${MY_EDC_FQDN} + +MY_EDC_IDS_BASE_URL=${MY_EDC_PROTOCOL}${MY_EDC_FQDN} +IDS_WEBHOOK_ADDRESS=${MY_EDC_IDS_BASE_URL} +EDC_IDS_ENDPOINT=${MY_EDC_IDS_BASE_URL}${WEB_HTTP_PROTOCOL_PATH} + +# Flyway Extension: Required +MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url +MY_EDC_JDBC_USER=missing-postgresql-user +MY_EDC_JDBC_PASSWORD=missing-postgresql-password + +# Flyway Extension: Defaults +EDC_DATASOURCE_DEFAULT_NAME=default +EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_ASSET_NAME=asset +EDC_DATASOURCE_ASSET_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_ASSET_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_ASSET_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_CONTRACTDEFINITION_NAME=contractdefinition +EDC_DATASOURCE_CONTRACTDEFINITION_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_CONTRACTDEFINITION_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_CONTRACTDEFINITION_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_CONTRACTNEGOTIATION_NAME=contractnegotiation +EDC_DATASOURCE_CONTRACTNEGOTIATION_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_CONTRACTNEGOTIATION_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_CONTRACTNEGOTIATION_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_POLICY_NAME=policy +EDC_DATASOURCE_POLICY_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_POLICY_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_POLICY_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_TRANSFERPROCESS_NAME=transferprocess +EDC_DATASOURCE_TRANSFERPROCESS_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_TRANSFERPROCESS_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_TRANSFERPROCESS_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_DATAPLANEINSTANCE_NAME=dataplaneinstance +EDC_DATASOURCE_DATAPLANEINSTANCE_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_DATAPLANEINSTANCE_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_DATAPLANEINSTANCE_PASSWORD=$MY_EDC_JDBC_PASSWORD + +# Broker Extension Configuration +POLICY_BROKER_BLACKLIST=REFERRING_CONNECTOR + +# Oauth default configurations +EDC_OAUTH_PROVIDER_AUDIENCE=idsc:IDS_CONNECTORS_ALL + +# This file could contain an entry replacing the EDC_KEYSTORE ENV var +# but for some reason it is required, and EDC won't start up if it isn't configured +# it is created in the Dockerfile +EDC_VAULT=/emtpy-properties-file.properties diff --git a/connector/Dockerfile b/connector/Dockerfile new file mode 100644 index 000000000..2ef8eba55 --- /dev/null +++ b/connector/Dockerfile @@ -0,0 +1,43 @@ +FROM gradle:7.6.0-jdk17 AS build + +ARG USERNAME +ARG TOKEN +ARG BUILD_ARGS + +ENV USERNAME=$USERNAME +ENV TOKEN=$TOKEN + +COPY --chown=gradle:gradle . /home/gradle/project/ +WORKDIR /home/gradle/project/ +RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle build --no-daemon $BUILD_ARGS + +# -buster is required to have apt available +FROM openjdk:17-slim-buster + +# Optional JVM arguments, such as memory settings +ARG JVM_ARGS="" + +# Install curl, then delete apt indexes to save image space +RUN apt update \ + && apt install -y curl \ + && rm -rf /var/cache/apt/archives /var/lib/apt/lists \ + && touch /emtpy-properties-file.properties + +WORKDIR /app + +COPY --from=build /home/gradle/project/connector/build/libs/app.jar /app +COPY ./connector/src/main/resources/logging.properties /app + +# health status is determined by the availability of the /health endpoint +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "X-Api-Key: $EDC_API_AUTH_KEY" --fail http://localhost:11002/api/v1/management/check/health + +# Use "exec" for graceful termination (SIGINT) to reach JVM. +# ARG can not be used in ENTRYPOINT so storing values in ENV variables +ENV JVM_ARGS=$JVM_ARGS + +# Read ENV Vars from .env with substitution +COPY ./connector/.env /app/.env + +# Replaces ENV Var statements so they don't overwrite existing ENV Vars +RUN sed -ri 's/^\s*(\S+)=(.*)$/\1=${\1:-"\2"}/' .env +ENTRYPOINT bash -c 'set -a && source /app/.env && set +a && exec java -Djava.util.logging.config.file=/app/logging.properties $JVM_ARGS -jar app.jar' diff --git a/connector/README.md b/connector/README.md new file mode 100644 index 000000000..74644f02b --- /dev/null +++ b/connector/README.md @@ -0,0 +1,43 @@ + +
+
+ + Logo + + +

Broker Server:
Docker Images

+ +

+ Report Bug + · + Request Feature +

+
+ +## Broker Server Image + +The Broker Server Extension together with other EDC Extensions are built into Docker Images. + +## Different Image Types + +Our EDC Community Edition builds several docker images in different configurations. + +| Docker Image | Type | Purpose | Features | +|-------------------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [broker-server-dev](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-dev) | Development |
  • Lightweight local development
  • Used in EDC UI's Getting Started section
|
  • IDS Broker Server Extension(s)
  • Management API Auth via API Keys
  • Mock IAM
| +| [broker-server-ce](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-ce) | Community Edition |
  • Deploy the Broker Server
|
  • IDS Broker Server Extension(s)
  • Management API Auth via API Keys
  • DAPS Authentication
  • PostgreSQL Persistence & Flyway
| + +## Image Tags + +| Tag | Description | +|---------|-----------------------------------| +| latest | latest version of our main branch | +| release | latest release of this repository | + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts new file mode 100644 index 000000000..57fe93b9c --- /dev/null +++ b/connector/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +val edcVersion: String by project +val edcGroup: String by project + +val sovityEdcExtensionsGroup: String by project +val sovityEdcExtensionsVersion: String by project + +dependencies { + // Control-Plane + implementation("${edcGroup}:control-plane-core:${edcVersion}") + implementation("${edcGroup}:management-api:${edcVersion}") + implementation("${edcGroup}:api-observability:${edcVersion}") + implementation("${edcGroup}:configuration-filesystem:${edcVersion}") + implementation("${edcGroup}:control-plane-aggregate-services:${edcVersion}") + implementation("${edcGroup}:http:${edcVersion}") + implementation("${edcGroup}:ids:${edcVersion}") + + // JDK Logger + implementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") + + // Optional: PostgreSQL + Flyway + if (project.hasProperty("postgres-flyway")) { + implementation("${sovityEdcExtensionsGroup}:postgres-flyway:${sovityEdcExtensionsVersion}") + } + + // Optional: Connector-To-Connector IAM + if (project.hasProperty("oauth2")) { + implementation("${edcGroup}:oauth2-core:${edcVersion}") + } else { + implementation("${edcGroup}:iam-mock:${edcVersion}") + } +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} diff --git a/connector/src/main/resources/logging.properties b/connector/src/main/resources/logging.properties new file mode 100644 index 000000000..b4d12f28f --- /dev/null +++ b/connector/src/main/resources/logging.properties @@ -0,0 +1,7 @@ +handlers = java.util.logging.ConsoleHandler +.level = INFO +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s %6$s%n +org.eclipse.dataspaceconnector.level = FINE +org.eclipse.dataspaceconnector.handler = java.util.logging.ConsoleHandler diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..7327f72c7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,61 @@ +version: "3.8" +services: + edc: + build: + dockerfile: connector/Dockerfile + depends_on: + - postgresql + environment: + MY_EDC_NAME_KEBAB_CASE: "example-connector" + MY_EDC_TITLE: "EDC Connector" + MY_EDC_DESCRIPTION: "Community Edition EDC Connector by sovity" + MY_EDC_CURATOR_URL: "https://example.com" + MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_MAINTAINER_URL: "https://sovity.de" + MY_EDC_MAINTAINER_NAME: "sovity GmbH" + + # Data Management API Key + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + # Data Space Configuration + EDC_OAUTH_CLIENT_ID: + EDC_OAUTH_TOKEN_URL: https://daps.test.mobility-dataspace.eu/token + EDC_OAUTH_PROVIDER_JWKS_URL: https://daps.test.mobility-dataspace.eu/jwks.json + EDC_BROKER_BASE_URL: https://broker.test.mobility-dataspace.eu + EDC_CLEARINGHOUSE_LOG_URL: https://clearing.test.mobility-dataspace.eu/messages/log + EDC_KEYSTORE: /secrets/keystore.jks + EDC_KEYSTORE_PASSWORD: password + EDC_OAUTH_CERTIFICATE_ALIAS: 1 + EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 + + MY_EDC_PROTOCOL: "http://" + MY_EDC_FQDN: "edc" + MY_EDC_IDS_BASE_URL: "http://edc:11003" + + MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' + EDC_WEB_REST_CORS_ORIGINS: '*' + ports: + - '11001:11001' + - '11002:11002' + - '11003:11003' + - '11004:11004' + - '11005:5005' + volumes: + - ./docs/getting-started/secrets:/secrets + postgresql: + image: docker.io/bitnami/postgresql:11 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + volumes: + - 'postgresql:/bitnami/postgresql' + +volumes: + postgresql: + driver: local diff --git a/resources/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml similarity index 94% rename from resources/checkstyle-config.xml rename to docs/dev/checkstyle/checkstyle-config.xml index a5d01d00f..9137f854c 100644 --- a/resources/checkstyle-config.xml +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -1,7 +1,7 @@ + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> - + @@ -29,8 +29,9 @@ - - + + @@ -48,9 +49,9 @@ + - + @@ -221,8 +222,7 @@ - + @@ -278,8 +278,10 @@ + + @@ -332,6 +334,10 @@ value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/> + + + + @@ -376,7 +382,7 @@ + default="checkstyle-xpath-suppressions.xml" /> diff --git a/extensions/broker-server/README.md b/extensions/broker-server/README.md new file mode 100644 index 000000000..2da3e724f --- /dev/null +++ b/extensions/broker-server/README.md @@ -0,0 +1,38 @@ + +
+
+ + Logo + + +

EDC-Connector Extension:
Broker Server

+ +

+ Report Bug + · + Request Feature +

+
+ +## About this Extension + +Implementation of an IDS Broker Server as an EDC Extension. + +This extension does multiple things: + +- Storage of Connectors and Data Offers +- Connector Crawling +- Connector Discovery +- API implementations for our the full management capabilities of our Broker UI + +## Why does this extension exist? + +To let the broker easily be a part of a data space we are implementing it on an EDC basis. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts new file mode 100644 index 000000000..58a8fd9dc --- /dev/null +++ b/extensions/broker-server/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + `java-library` +} + +val edcVersion: String by project +val edcGroup: String by project +val jupiterVersion: String by project +val mockitoVersion: String by project +val assertj: String by project +val okHttpVersion: String by project + +dependencies { + annotationProcessor("org.projectlombok:lombok:1.18.26") + compileOnly("org.projectlombok:lombok:1.18.26") + + implementation("${edcGroup}:control-plane-core:${edcVersion}") + implementation("${edcGroup}:management-api-configuration:${edcVersion}") + implementation("${edcGroup}:ids-spi:${edcVersion}") + implementation("${edcGroup}:ids-api-multipart-dispatcher-v1:${edcVersion}") + implementation("${edcGroup}:ids-api-configuration:${edcVersion}") + implementation("${edcGroup}:ids-jsonld-serdes:${edcVersion}") + + implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") + + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-params:${jupiterVersion}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}") +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +tasks.register("prepareKotlinBuildScriptModel"){} + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java new file mode 100644 index 000000000..0bc86f516 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +public class BrokerServerExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "BrokerServerExtension"; + + @Inject + private ManagementApiConfiguration managementApiConfiguration; + + @Inject + private WebService webService; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var brokerServerExtensionContext = BrokerServerExtensionContextBuilder.buildContext(); + + String managementApiGroup = managementApiConfiguration.getContextAlias(); + brokerServerExtensionContext.jaxRsResources().forEach(resource -> + webService.registerResource(managementApiGroup, resource)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java new file mode 100644 index 000000000..f5a71dd24 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import java.util.List; + + +/** + * Manual Dependency Injection result + * + * @param jaxRsResources Jax RS Resource implementations to register. + */ +public record BrokerServerExtensionContext(List jaxRsResources) { +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java new file mode 100644 index 000000000..9c9a5e54a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import lombok.NoArgsConstructor; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; + +import java.util.List; + + +/** + * Manual Dependency Injection. + *

+ * We want to develop as Java Backend Development is done, but we have + * no CDI / DI Framework to rely on. + *

+ * EDC {@link Inject} only works in {@link BrokerServerExtension}. + */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class BrokerServerExtensionContextBuilder { + public static BrokerServerExtensionContext buildContext() { + return new BrokerServerExtensionContext(List.of()); + } +} diff --git a/extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..80d56e9c3 --- /dev/null +++ b/extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.ext.brokerserver.BrokerServerExtension diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..8ffeb6ed9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,24 @@ +# Broker +sovityBrokerServerGroup=de.sovity.broker +sovityBrokerServerVersion=0.0.1-SNAPSHOT + +# Sovity EDC Extensions +sovityEdcExtensionsVersion=3.1.1-SNAPSHOT +sovityEdcExtensionsGroup=de.sovity.edc.ext + +# Eclipse EDC +edcGroup=org.eclipse.edc +edcVersion=0.0.1-20230220.patch1 + +# Other Dependencies +assertj=3.23.1 +jupiterVersion=5.8.2 +mockitoVersion=4.8.0 +okHttpVersion=4.10.0 +jsonVersion=20220924 +restAssured=4.5.0 +flywayVersion=9.0.1 +postgresVersion=42.4.0 + +# Other Properties +org.gradle.jvmargs=-Xmx1024m diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..41dfb8790 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..1b6c78733 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..c480d3dd4 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,4 @@ +rootProject.name = "edc-broker-server-extension" + +include(":extensions:broker-server") +include(":connector") From 26b2965e2c7ab26f6f23edbed8e862e1dbdccb37 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 10 May 2023 13:05:50 +0200 Subject: [PATCH 008/295] chore remove README.md project shields due to private repository (#23) --- README.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/README.md b/README.md index 972f2ee8c..1122cc86e 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,6 @@ - - -[![Contributors][contributors-shield]][contributors-url] -[![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] -[![Issues][issues-shield]][issues-url] -[![Apache 2.0][license-shield]][license-url] -[![LinkedIn][linkedin-shield]][linkedin-url] -

@@ -70,36 +62,3 @@ Distributed under the Apache 2.0 License. See `LICENSE` for more information. contact@sovity.de

(back to top)

- - - - -[contributors-shield]: -https://img.shields.io/github/contributors/sovity/edc-broker-server-extension.svg?style=for-the-badge - -[contributors-url]: https://github.com/sovity/edc-broker-server-extension/graphs/contributors - -[forks-shield]: -https://img.shields.io/github/forks/sovity/edc-broker-server-extension.svg?style=for-the-badge - -[forks-url]: https://github.com/sovity/edc-broker-server-extension/network/members - -[stars-shield]: -https://img.shields.io/github/stars/sovity/edc-broker-server-extension.svg?style=for-the-badge - -[stars-url]: https://github.com/sovity/edc-broker-server-extension/stargazers - -[issues-shield]: -https://img.shields.io/github/issues/sovity/edc-broker-server-extension.svg?style=for-the-badge - -[issues-url]: https://github.com/sovity/edc-broker-server-extension/issues - -[license-shield]: -https://img.shields.io/github/license/sovity/edc-broker-server-extension.svg?style=for-the-badge - -[license-url]: https://github.com/sovity/edc-broker-server-extension/blob/master/LICENSE.txt - -[linkedin-shield]: -https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 - -[linkedin-url]: https://www.linkedin.com/company/sovity From 303c9f267359d118bfa1dc42ea99dabc40b23ba5 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 11 May 2023 09:06:25 +0200 Subject: [PATCH 009/295] feat: connector and contractoffer models and stores (#24) --- .../dao/models/ConnectorRecord.java | 42 +++++++++++++++++++ .../dao/models/ContractOfferRecord.java | 18 ++++++++ .../brokerserver/dao/models/OnlineStatus.java | 20 +++++++++ .../dao/stores/InMemoryConnectorStore.java | 37 ++++++++++++++++ .../stores/InMemoryContractOfferStore.java | 37 ++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java new file mode 100644 index 000000000..0a917aafb --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; + +@Getter +@ToString +@Builder(toBuilder = true) +@EqualsAndHashCode(of = "id") +@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) +@FieldDefaults(makeFinal = true, level = lombok.AccessLevel.PRIVATE) +public class ConnectorRecord { + String id; + String idsId; + String title; + String description; + String endpoint; + OffsetDateTime lastUpdate; + OffsetDateTime offlineSince; + OffsetDateTime createdAt; + OnlineStatus onlineStatus; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java new file mode 100644 index 000000000..15cb4a443 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java @@ -0,0 +1,18 @@ +package de.sovity.edc.ext.brokerserver.dao.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; + +@Getter +@ToString +@Builder(toBuilder = true) +@EqualsAndHashCode(of = "id") +@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) +@FieldDefaults(makeFinal = true, level = lombok.AccessLevel.PRIVATE) +public class ContractOfferRecord { + String id; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java new file mode 100644 index 000000000..e6d21c798 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +public enum OnlineStatus { + ONLINE, + OFFLINE +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java new file mode 100644 index 000000000..c6f6f410f --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.stores; + +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class InMemoryConnectorStore { + private final Map connectorsById = new HashMap<>(); + + public Stream findAll() { + return connectorsById.values().stream(); + } + + public ConnectorRecord findById(String connectorId) { + return connectorsById.get(connectorId); + } + + public ConnectorRecord save(ConnectorRecord connector) { + return connectorsById.put(connector.getId(), connector); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java new file mode 100644 index 000000000..f73e00a32 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.stores; + +import de.sovity.edc.ext.brokerserver.dao.models.ContractOfferRecord; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class InMemoryContractOfferStore { + private final Map contractOffersById = new HashMap<>(); + + public Stream findAll() { + return contractOffersById.values().stream(); + } + + public ContractOfferRecord findById(String contractOfferId) { + return contractOffersById.get(contractOfferId); + } + + public ContractOfferRecord save(ContractOfferRecord contractOffer) { + return contractOffersById.put(contractOffer.getId(), contractOffer); + } +} From d944e5030317ecca5978755311f72c1d35c59b45 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 11 May 2023 11:24:06 +0200 Subject: [PATCH 010/295] feat: initialize broker on startup (#28) --- .../brokerserver/BrokerServerExtension.java | 9 +++- .../BrokerServerExtensionContext.java | 6 ++- .../BrokerServerExtensionContextBuilder.java | 9 +++- ...onnectorStore.java => ConnectorStore.java} | 2 +- ...fferStore.java => ContractOfferStore.java} | 2 +- .../services/BrokerServerInitializer.java | 47 +++++++++++++++++++ 6 files changed, 68 insertions(+), 7 deletions(-) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/{InMemoryConnectorStore.java => ConnectorStore.java} (96%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/{InMemoryContractOfferStore.java => ContractOfferStore.java} (96%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 0bc86f516..957f8b1da 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -16,6 +16,7 @@ import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; @@ -24,6 +25,9 @@ public class BrokerServerExtension implements ServiceExtension { public static final String EXTENSION_NAME = "BrokerServerExtension"; + @Setting + public static final String KNOWN_CONNECTORS = "edc.brokerserver.known.connectors"; + @Inject private ManagementApiConfiguration managementApiConfiguration; @@ -37,10 +41,11 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var brokerServerExtensionContext = BrokerServerExtensionContextBuilder.buildContext(); + var services = BrokerServerExtensionContextBuilder.buildContext(context.getConfig()); + services.brokerServerInitializer().initializeConnectorList(); String managementApiGroup = managementApiConfiguration.getContextAlias(); - brokerServerExtensionContext.jaxRsResources().forEach(resource -> + services.jaxRsResources().forEach(resource -> webService.registerResource(managementApiGroup, resource)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index f5a71dd24..57d05ef99 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -14,6 +14,8 @@ package de.sovity.edc.ext.brokerserver; +import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; + import java.util.List; @@ -22,5 +24,7 @@ * * @param jaxRsResources Jax RS Resource implementations to register. */ -public record BrokerServerExtensionContext(List jaxRsResources) { +public record BrokerServerExtensionContext( + List jaxRsResources, + BrokerServerInitializer brokerServerInitializer) { } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 9c9a5e54a..d6ed5f08b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -14,8 +14,11 @@ package de.sovity.edc.ext.brokerserver; +import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import lombok.NoArgsConstructor; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.configuration.Config; import java.util.List; @@ -30,7 +33,9 @@ */ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class BrokerServerExtensionContextBuilder { - public static BrokerServerExtensionContext buildContext() { - return new BrokerServerExtensionContext(List.of()); + public static BrokerServerExtensionContext buildContext(Config config) { + var connectorStore = new ConnectorStore(); + var brokerServerInitializer = new BrokerServerInitializer(connectorStore, config); + return new BrokerServerExtensionContext(List.of(), brokerServerInitializer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java similarity index 96% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java index c6f6f410f..91eb7022c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryConnectorStore.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.stream.Stream; -public class InMemoryConnectorStore { +public class ConnectorStore { private final Map connectorsById = new HashMap<>(); public Stream findAll() { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java similarity index 96% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java index f73e00a32..5a19681f9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/InMemoryContractOfferStore.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.stream.Stream; -public class InMemoryContractOfferStore { +public class ContractOfferStore { private final Map contractOffersById = new HashMap<>(); public Stream findAll() { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java new file mode 100644 index 000000000..2fd35ebd8 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.time.OffsetDateTime; +import java.util.Arrays; + +@RequiredArgsConstructor +public class BrokerServerInitializer { + private final ConnectorStore connectorStore; + private final Config config; + + public void initializeConnectorList() { + var knownConnectors = config.getString(BrokerServerExtension.KNOWN_CONNECTORS).split(","); + + Arrays.stream(knownConnectors).forEach(connectorId -> { + connectorId = connectorId.trim(); + + var connectorRecord = ConnectorRecord.builder() + .id(connectorId) + .idsId(connectorId) + .endpoint(connectorId) + .createdAt(OffsetDateTime.now()) + .build(); + + connectorStore.save(connectorRecord); + }); + } +} From ec7c6f2e94ca42987424e4cb53d5641c6bae6d5e Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 12 May 2023 07:37:31 +0200 Subject: [PATCH 011/295] feat: api endpoint stubs, some project structure, log entry model (#33) --- .gitattributes | 389 ++-------------- .gitignore | 425 +----------------- LICENSE.md | 2 +- connector/build.gradle.kts | 4 +- extensions/broker-server/build.gradle.kts | 5 + .../brokerserver/BrokerServerExtension.java | 5 +- .../BrokerServerExtensionContext.java | 13 +- .../BrokerServerExtensionContextBuilder.java | 26 +- .../BrokerServerResourceImpl.java | 44 ++ ...Status.java => ConnectorOnlineStatus.java} | 2 +- .../dao/models/ConnectorRecord.java | 7 +- .../dao/models/ContractOfferRecord.java | 19 + .../dao/models/LogEventRecord.java | 79 ++++ .../dao/models/LogEventStatus.java | 35 ++ .../brokerserver/dao/models/LogEventType.java | 51 +++ .../dao/stores/ConnectorStore.java | 2 + .../dao/stores/ContractOfferStore.java | 2 + .../dao/stores/LogEventStore.java | 40 ++ .../services/BrokerEventLogger.java | 52 +++ .../services/api/CatalogApiService.java | 30 ++ .../services/api/ConnectorApiService.java | 81 ++++ .../services/api/PaginationMetadataUtils.java | 28 ++ .../ext/brokerserver/services/queue/.gitkeep | 0 .../ConnectorSelfDescriptionFetcher.java | 34 ++ .../services/refreshing/ConnectorUpdater.java | 38 ++ .../refreshing/ContractOfferFetcher.java | 33 ++ gradle.properties | 3 +- 27 files changed, 675 insertions(+), 774 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/{OnlineStatus.java => ConnectorOnlineStatus.java} (92%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/.gitkeep create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java diff --git a/.gitattributes b/.gitattributes index 067d366da..8510e87df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,335 +1,40 @@ -# This tells Git to enforce Unix-style line endings across all clones of the repository, preventing various issues that might arise from developing on Windows for Linux. https://help.github.com/articles/dealing-with-line-endings/ * text=auto eol=lf -# Created with https://gitattributes.io -# Edit at https://gitattributes.io/api/web%2Cjava%2Cc%2B%2B%2Ccsharp%2Ccommon%2Cgo%2Cvisualstudio - -# Sources -*.c text diff=c -*.cc text diff=cpp -*.cxx text diff=cpp -*.cpp text diff=cpp -*.c++ text diff=cpp -*.hpp text diff=cpp -*.h text diff=c -*.h++ text diff=cpp -*.hh text diff=cpp - -# Compiled Object files -*.slo binary -*.lo binary -*.o binary -*.obj binary - -# Precompiled Headers -*.gch binary -*.pch binary - -# Compiled Dynamic libraries -*.so binary -*.dylib binary -*.dll binary - -# Compiled Static libraries -*.lai binary -*.la binary -*.a binary -*.lib binary - -# Executables -*.exe binary -*.out binary -*.app binary -# Common settings that generally should always be used with your language specific settings - -# Auto detect text files and perform LF normalization -# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ -* text=auto - -# -# The above will handle all files NOT found below -# - -# Documents -*.bibtex text diff=bibtex -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain -*.md text -*.tex text diff=tex -*.adoc text -*.textile text -*.mustache text -*.csv text -*.tab text -*.tsv text -*.txt text -*.sql text +# Web +*.css text diff=css +*.scss text diff=css +*.htm text diff=html +*.html text diff=html +*.properties text eol=lf -# Graphics -*.png binary -*.jpg binary -*.jpeg binary -*.gif binary -*.tif binary -*.tiff binary -*.ico binary -# SVG treated as an asset (binary) by default. -*.svg text -# If you want to treat it as binary, -# use the following line instead. -# *.svg binary -*.eps binary +# Exclude files from exporting +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore # Scripts *.bash text eol=lf *.fish text eol=lf *.sh text eol=lf -# These are explicitly windows files and should use crlf + +# Windows Scripts need crlf *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf -# Serialisation -*.json text -*.toml text -*.xml text -*.yaml text -*.yml text - -# Archives -*.7z binary -*.gz binary -*.tar binary -*.tgz binary -*.zip binary - -# Text files where line endings should be preserved -*.patch -text - -# -# Exclude files from exporting -# - -.gitattributes export-ignore -.gitignore export-ignore -# Auto detect text files and perform LF normalization -# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ -* text=auto - -*.cs text diff=csharp - -# Go sources -*.go text diff=golang -*.mod text eol=lf -*.sum text eol=lf - -# Java sources -*.java text diff=java -*.gradle text diff=java -*.gradle.kts text diff=java - -# These files are text and should be normalized (Convert crlf => lf) -*.css text diff=css -*.df text -*.htm text diff=html -*.html text diff=html -*.js text -*.jsp text -*.jspf text -*.jspx text -*.properties text -*.tld text -*.tag text -*.tagx text -*.xml text - -# These files are binary and should be left untouched -# (binary is a macro for -text -diff) -*.class binary -*.dll binary -*.ear binary -*.jar binary -*.so binary -*.war binary -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just comment the entries below and -# uncomment the group further below -############################################################################### - -*.sln text eol=crlf -*.csproj text eol=crlf -*.vbproj text eol=crlf -*.vcxproj text eol=crlf -*.vcproj text eol=crlf -*.dbproj text eol=crlf -*.fsproj text eol=crlf -*.lsproj text eol=crlf -*.wixproj text eol=crlf -*.modelproj text eol=crlf -*.sqlproj text eol=crlf -*.wmaproj text eol=crlf - -*.xproj text eol=crlf -*.props text eol=crlf -*.filters text eol=crlf -*.vcxitems text eol=crlf - - -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -#*.xproj merge=binary -#*.props merge=binary -#*.filters merge=binary -#*.vcxitems merge=binary -## GITATTRIBUTES FOR WEB PROJECTS -# -# These settings are for any web project. -# -# Details per file setting: -# text These files should be normalized (i.e. convert CRLF to LF). -# binary These files are binary and should be left untouched. -# -# Note that binary is a macro for -text -diff. -###################################################################### - -# Auto detect -## Handle line endings automatically for files detected as -## text and leave all files detected as binary untouched. -## This will handle all files NOT defined below. -* text=auto - -# Source code -*.bash text eol=lf -*.bat text eol=crlf -*.cmd text eol=crlf -*.coffee text -*.css text -*.htm text diff=html -*.html text diff=html -*.inc text -*.ini text -*.js text -*.json text -*.jsx text -*.less text -*.ls text -*.map text -diff -*.od text -*.onlydata text -*.php text diff=php -*.pl text -*.ps1 text eol=crlf -*.py text diff=python -*.rb text diff=ruby -*.sass text -*.scm text -*.scss text diff=css -*.sh text eol=lf -*.sql text -*.styl text -*.tag text -*.ts text -*.tsx text -*.xml text -*.xhtml text diff=html - -# Docker -Dockerfile text - -# Documentation -*.ipynb text -*.markdown text -*.md text -*.mdwn text -*.mdown text -*.mkd text -*.mkdn text -*.mdtxt text -*.mdtext text -*.txt text -AUTHORS text -CHANGELOG text -CHANGES text -CONTRIBUTING text -COPYING text -copyright text -*COPYRIGHT* text -INSTALL text -license text -LICENSE text -NEWS text -readme text -*README* text -TODO text - -# Templates -*.dot text -*.ejs text -*.haml text -*.handlebars text -*.hbs text -*.hbt text -*.jade text -*.latte text -*.mustache text -*.njk text -*.phtml text -*.tmpl text -*.tpl text -*.twig text -*.vue text - -# Configs -*.cnf text -*.conf text -*.config text -.editorconfig text -.env text -.gitattributes text -.gitconfig text -.htaccess text -*.lock text -diff -package-lock.json text -diff -*.toml text -*.yaml text -*.yml text -browserslist text -Makefile text -makefile text - -# Heroku -Procfile text +# Documents +*.tex text diff=tex +*.bibtex text diff=bibtex +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain # Graphics *.ai binary @@ -348,17 +53,26 @@ Procfile text *.png binary *.psb binary *.psd binary -# SVG treated as an asset (binary) by default. -*.svg text -# If you want to treat it as binary, -# use the following line instead. -# *.svg binary *.svgz binary *.tif binary *.tiff binary *.wbmp binary *.webp binary +# Fonts +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +# Archives +*.7z binary +*.gz binary +*.tar binary +*.tgz binary +*.zip binary + # Audio *.kar binary *.m4a binary @@ -386,28 +100,3 @@ Procfile text *.swc binary *.swf binary *.webm binary - -# Archives -*.7z binary -*.gz binary -*.jar binary -*.rar binary -*.tar binary -*.zip binary - -# Fonts -*.ttf binary -*.eot binary -*.otf binary -*.woff binary -*.woff2 binary - -# Executables -*.exe binary -*.pyc binary - -# RC files (like .babelrc or .eslintrc) -*.*rc text - -# Ignore files (like .npmignore or .gitignore) -*.*ignore text diff --git a/.gitignore b/.gitignore index 48a56f90d..ac749579b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,184 +1,6 @@ -# Created by https://www.toptal.com/developers/gitignore/api/java,node,intellij+all,intellij,windows,linux,maven,gradle -# Edit at https://www.toptal.com/developers/gitignore?templates=java,node,intellij+all,intellij,windows,linux,maven,gradle - -### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -# Azure Toolkit for IntelliJ plugin -# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij -.idea/**/azureSettings.xml - -### Intellij+all ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff - -# AWS User-specific - -# Generated files - -# Sensitive or high-churn files - -# Gradle - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake - -# Mongo Explorer plugin - -# File-based project format - -# IntelliJ - -# mpeltonen/sbt-idea plugin - -# JIRA plugin - -# Cursive Clojure plugin - -# SonarLint plugin - -# Crashlytics plugin (for Android Studio and IntelliJ) - -# Editor-based Rest Client - -# Android studio 3.1+ serialized cache file - -### Intellij+all Patch ### -# Ignore everything but code style settings and run configurations -# that are supposed to be shared within teams. - -.idea/* - -!.idea/codeStyles -!.idea/runConfigurations - -### Java ### # Compiled class file *.class -# Log file -*.log - # BlueJ files *.ctxt @@ -187,6 +9,7 @@ fabric.properties # Package Files # *.jar +!gradle/wrapper/gradle-wrapper.jar *.war *.nar *.ear @@ -196,239 +19,31 @@ fabric.properties # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -replay_pid* - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### Maven ### -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar - -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - -### Node ### -# Logs -logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -### Node Patch ### -# Serverless Webpack directories -.webpack/ - -# Optional stylelint cache - -# SvelteKit build / generate output -.svelte-kit - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -### Gradle ### +# Ignore Gradle project-specific cache directory .gradle -**/build/ -!src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar +# Ignore Gradle build output directory +build -# Avoid ignore Gradle wrappper properties -!gradle-wrapper.properties +.idea +*.iml +.run +.vs +.vscode -# Cache of project -.gradletasknamecache +.DS_Store -# Eclipse Gradle plugin generated files -# Eclipse Core -# JDT-specific (Eclipse Java Development Tools) - -### Gradle Patch ### -# Java heap dump +**/out *.hprof -# End of https://www.toptal.com/developers/gitignore/api/java,node,intellij+all,intellij,windows,linux,maven,gradle +**/.env +!.env +!connector/.env + +# Log files +*.log -/.idea/.gitignore -/.idea/checkstyle-idea.xml -/.idea/github-templates.iml -/.idea/sonarlint/issuestore/index.pb -/.idea/jpa-buddy.xml -/.idea/misc.xml -/.idea/modules.xml -/.idea/vcs.xml +**/*.key +**/*.p12 +**/*.jks diff --git a/LICENSE.md b/LICENSE.md index 46a734a70..8dc66084c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2022 sovity.de +Copyright 2023 sovity.de Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index 57fe93b9c..c00c858ed 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -7,7 +7,7 @@ plugins { val edcVersion: String by project val edcGroup: String by project -val sovityEdcExtensionsGroup: String by project +val sovityEdcExtensionGroup: String by project val sovityEdcExtensionsVersion: String by project dependencies { @@ -25,7 +25,7 @@ dependencies { // Optional: PostgreSQL + Flyway if (project.hasProperty("postgres-flyway")) { - implementation("${sovityEdcExtensionsGroup}:postgres-flyway:${sovityEdcExtensionsVersion}") + implementation("${sovityEdcExtensionGroup}:postgres-flyway:${sovityEdcExtensionsVersion}") } // Optional: Connector-To-Connector IAM diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 58a8fd9dc..ac316c091 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -8,10 +8,13 @@ val jupiterVersion: String by project val mockitoVersion: String by project val assertj: String by project val okHttpVersion: String by project +val sovityEdcGroup: String by project +val sovityEdcExtensionsVersion: String by project dependencies { annotationProcessor("org.projectlombok:lombok:1.18.26") compileOnly("org.projectlombok:lombok:1.18.26") + implementation("org.apache.commons:commons-lang3:3.12.0") implementation("${edcGroup}:control-plane-core:${edcVersion}") implementation("${edcGroup}:management-api-configuration:${edcVersion}") @@ -20,6 +23,8 @@ dependencies { implementation("${edcGroup}:ids-api-configuration:${edcVersion}") implementation("${edcGroup}:ids-jsonld-serdes:${edcVersion}") + api("${sovityEdcGroup}:wrapper-broker-api:${sovityEdcExtensionsVersion}") + implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") testImplementation("org.assertj:assertj-core:${assertj}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 957f8b1da..6a863b893 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 sovity GmbH + * Copyright (c) 2023 sovity GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -45,7 +45,6 @@ public void initialize(ServiceExtensionContext context) { services.brokerServerInitializer().initializeConnectorList(); String managementApiGroup = managementApiConfiguration.getContextAlias(); - services.jaxRsResources().forEach(resource -> - webService.registerResource(managementApiGroup, resource)); + webService.registerResource(managementApiGroup, services.brokerServerResource()); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index 57d05ef99..992718b86 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 sovity GmbH + * Copyright (c) 2023 sovity GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -15,16 +15,17 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; - -import java.util.List; +import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; /** * Manual Dependency Injection result * - * @param jaxRsResources Jax RS Resource implementations to register. + * @param brokerServerResource REST Resource with API Endpoint implementations + * @param brokerServerInitializer Startup Logic */ public record BrokerServerExtensionContext( - List jaxRsResources, - BrokerServerInitializer brokerServerInitializer) { + BrokerServerResource brokerServerResource, + BrokerServerInitializer brokerServerInitializer +) { } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index d6ed5f08b..b4ca84608 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 sovity GmbH + * Copyright (c) 2023 sovity GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -15,13 +15,15 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import de.sovity.edc.ext.brokerserver.dao.stores.ContractOfferStore; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; +import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; import lombok.NoArgsConstructor; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.configuration.Config; -import java.util.List; - /** * Manual Dependency Injection. @@ -34,8 +36,24 @@ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class BrokerServerExtensionContextBuilder { public static BrokerServerExtensionContext buildContext(Config config) { + // Dao var connectorStore = new ConnectorStore(); + var contractOfferStore = new ContractOfferStore(); + + // Services var brokerServerInitializer = new BrokerServerInitializer(connectorStore, config); - return new BrokerServerExtensionContext(List.of(), brokerServerInitializer); + + // UI Capabilities + var paginationMetadataUtils = new PaginationMetadataUtils(); + var catalogApiService = new CatalogApiService( + contractOfferStore, + paginationMetadataUtils + ); + var connectorApiService = new ConnectorApiService( + connectorStore, + paginationMetadataUtils + ); + var brokerServerResource = new BrokerServerResourceImpl(connectorApiService, catalogApiService); + return new BrokerServerExtensionContext(brokerServerResource, brokerServerInitializer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java new file mode 100644 index 000000000..696fed3eb --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; +import lombok.RequiredArgsConstructor; + + +/** + * Implementation of {@link BrokerServerResource} + */ +@RequiredArgsConstructor +public class BrokerServerResourceImpl implements BrokerServerResource { + private final ConnectorApiService connectorApiService; + private final CatalogApiService catalogApiService; + + @Override + public CatalogPageResult catalogPage(CatalogPageQuery query) { + return catalogApiService.catalogPage(query); + } + + @Override + public ConnectorPageResult connectorPage(ConnectorPageQuery query) { + return connectorApiService.connectorPage(query); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java similarity index 92% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java index e6d21c798..a0840ce8c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/OnlineStatus.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.dao.models; -public enum OnlineStatus { +public enum ConnectorOnlineStatus { ONLINE, OFFLINE } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java index 0a917aafb..0b6445a69 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java @@ -23,6 +23,11 @@ import java.time.OffsetDateTime; +/** + * Connector Database Row that can be inserted or updated. + *

+ * Represents metadata for another connector in the dataspace. + */ @Getter @ToString @Builder(toBuilder = true) @@ -38,5 +43,5 @@ public class ConnectorRecord { OffsetDateTime lastUpdate; OffsetDateTime offlineSince; OffsetDateTime createdAt; - OnlineStatus onlineStatus; + ConnectorOnlineStatus onlineStatus; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java index 15cb4a443..6f34a2677 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.brokerserver.dao.models; import lombok.AllArgsConstructor; @@ -7,6 +21,11 @@ import lombok.ToString; import lombok.experimental.FieldDefaults; +/** + * Contract Offer Database Row that can be inserted or updated. + *

+ * Represents a data offer in the data space. + */ @Getter @ToString @Builder(toBuilder = true) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java new file mode 100644 index 000000000..660b44104 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; + +/** + * Log Event Database Row that can be inserted or updated. + *

+ * Many kinds of events or tasks might log into this table. + * Logging of execution times is also supported. + */ +@Getter +@ToString +@Builder(toBuilder = true) +@EqualsAndHashCode(of = "id") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +public class LogEventRecord { + /** + * Row ID + */ + String id; + + /** + * Log Message Date + */ + OffsetDateTime createdAt; + + /** + * Log Entry Type + */ + LogEventType type; + + /** + * Log Entry Type + */ + LogEventStatus status; + + /** + * Connector reference, if applicable + */ + String connectorId; + + /** + * Contract Offer reference, if applicable + */ + String contractOfferId; + + /** + * Message to be shown in UI, if applicable + */ + String userMessage; + + /** + * Execution time in milliseconds, if recorded / applicable + */ + Long executionTimeInMs; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java new file mode 100644 index 000000000..b99befa58 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +/** + * This enum allows us to differentiate errors and changeful events. + */ +public enum LogEventStatus { + /** + * Means that something failed. + */ + ERROR, + + /** + * Means that these log messages can basically be skipped. + */ + UNCHANGED, + + /** + * A log message that might interest the user (compared to log messages kept to calculate execution times) + */ + UPDATED; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java new file mode 100644 index 000000000..987173fc9 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +/** + * The table {@link LogEventRecord} table contains many types of events or task outcomes. + * This enum is used to distinguish between them. + */ +public enum LogEventType { + /** + * Connector was successfully updated, and changes were incorporated + */ + CONNECTOR_UPDATED, + + /** + * Connector went online + */ + CONNECTOR_STATUS_CHANGE_ONLINE, + + /** + * Connector went offline + */ + CONNECTOR_STATUS_CHANGE_OFFLINE, + + /** + * Connector was "force deleted" + */ + CONNECTOR_STATUS_CHANGE_FORCE_DELETED, + + /** + * Contract Offer was updated + */ + CONTRACT_OFFER_UPDATED, + + /** + * Contract Offer was clicked + */ + CONTRACT_OFFER_CLICK; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java index 91eb7022c..622604be1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.dao.stores; import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; +import org.apache.commons.lang3.Validate; import java.util.HashMap; import java.util.Map; @@ -32,6 +33,7 @@ public ConnectorRecord findById(String connectorId) { } public ConnectorRecord save(ConnectorRecord connector) { + Validate.notBlank(connector.getId(), "Need Connector ID"); return connectorsById.put(connector.getId(), connector); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java index 5a19681f9..0cad9830b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.dao.stores; import de.sovity.edc.ext.brokerserver.dao.models.ContractOfferRecord; +import org.apache.commons.lang3.Validate; import java.util.HashMap; import java.util.Map; @@ -32,6 +33,7 @@ public ContractOfferRecord findById(String contractOfferId) { } public ContractOfferRecord save(ContractOfferRecord contractOffer) { + Validate.notBlank(contractOffer.getId(), "Need Contract Offer ID"); return contractOffersById.put(contractOffer.getId(), contractOffer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java new file mode 100644 index 000000000..0c697e763 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.stores; + +import de.sovity.edc.ext.brokerserver.dao.models.LogEventRecord; +import lombok.NonNull; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class LogEventStore { + private final Map logEntries = new HashMap<>(); + + public LogEventRecord save(@NonNull LogEventRecord logEntry) { + Validate.isTrue(logEntry.getId() == null, "ID already set!"); + var updated = logEntry.toBuilder().id(UUID.randomUUID().toString()).build(); + logEntries.put(updated.getId(), updated); + return updated; + } + + public List findAll() { + return new ArrayList<>(logEntries.values()); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java new file mode 100644 index 000000000..dd6e3e1ce --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.dao.models.LogEventRecord; +import de.sovity.edc.ext.brokerserver.dao.models.LogEventStatus; +import de.sovity.edc.ext.brokerserver.dao.models.LogEventType; +import de.sovity.edc.ext.brokerserver.dao.stores.LogEventStore; +import lombok.RequiredArgsConstructor; + +import java.time.OffsetDateTime; + +/** + * Updates a single connector. + */ +@RequiredArgsConstructor +public class BrokerEventLogger { + private final LogEventStore logEventStore; + + public void logConnectorUpdate(ConnectorRecord connector, LogEventStatus outcome) { + var logEntry = LogEventRecord.builder() + .connectorId(connector.getId()) + .userMessage(getConnectorUpdatedMessage(outcome)) + .type(LogEventType.CONNECTOR_UPDATED) + .createdAt(OffsetDateTime.now()) + .status(outcome) + .build(); + this.logEventStore.save(logEntry); + } + + private static String getConnectorUpdatedMessage(LogEventStatus outcome) { + return switch (outcome) { + case UNCHANGED -> "Connector is up to date"; + case UPDATED -> "Connector was updated"; + case ERROR -> "Connector update failed"; + default -> throw new IllegalArgumentException("Unknown outcome: " + outcome); + }; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java new file mode 100644 index 000000000..ac548f70b --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.dao.stores.ContractOfferStore; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CatalogApiService { + private final ContractOfferStore contractOfferStore; + private final PaginationMetadataUtils paginationMetadataUtils; + + public CatalogPageResult catalogPage(CatalogPageQuery query) { + throw new IllegalStateException("Not implemented yet"); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java new file mode 100644 index 000000000..80938996e --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingItem; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +public class ConnectorApiService { + private final ConnectorStore connectorStore; + private final PaginationMetadataUtils paginationMetadataUtils; + + public ConnectorPageResult connectorPage(ConnectorPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); + var result = new ConnectorPageResult(); + + var connectors = connectorStore.findAll() + .map(ConnectorApiService::buildConnectorListEntry) + .sorted(Comparator.comparing(ConnectorListEntry::getTitle)) + .toList(); + + result.setAvailableSortings(buildAvailableSortings()); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectors.size())); + result.setConnectors(connectors); + return result; + } + + @NotNull + private static ConnectorListEntry buildConnectorListEntry(ConnectorRecord it) { + ConnectorListEntry dto = new ConnectorListEntry(); + dto.setId(it.getId()); + dto.setIdsId(it.getIdsId()); + dto.setEndpoint(it.getEndpoint()); + dto.setTitle(it.getTitle()); + dto.setDescription(it.getDescription()); + dto.setCreatedAt(it.getCreatedAt()); + dto.setLastFetchAt(it.getLastUpdate()); + dto.setOnlineStatus(getOnlineStatus(it)); + dto.setOfflineSince(it.getOfflineSince()); + dto.setNumContractOffers(-1); + return dto; + } + + private static ConnectorOnlineStatus getOnlineStatus(ConnectorRecord it) { + return switch (it.getOnlineStatus()) { + case ONLINE -> ConnectorOnlineStatus.ONLINE; + case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + it.getOnlineStatus()); + }; + } + + @NotNull + private static List buildAvailableSortings() { + return Arrays.stream(ConnectorPageSortingType.values()).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java new file mode 100644 index 000000000..8d20e3701 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.wrapper.api.broker.model.PaginationMetadata; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor +public class PaginationMetadataUtils { + + @NotNull + public PaginationMetadata buildDummyPaginationMetadata(int numResults) { + return new PaginationMetadata(numResults, numResults, 1, numResults); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/.gitkeep b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java new file mode 100644 index 000000000..b874cecae --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; + +/** + * Fetch Connector Metadata. + */ +public class ConnectorSelfDescriptionFetcher { + + /** + * Fetches Connector metadata and returns an updated {@link ConnectorRecord} + * + * @param connector existing / stubbed connector db row + * @return updated connector db row + */ + public ConnectorRecord updateConnector(ConnectorRecord connector) { + // TODO implement + throw new IllegalArgumentException("Not yet implemented"); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java new file mode 100644 index 000000000..90977e6a2 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; +import lombok.RequiredArgsConstructor; + +/** + * Updates a single connector. + */ +@RequiredArgsConstructor +public class ConnectorUpdater { + private final ConnectorSelfDescriptionFetcher connectorSelfDescriptionFetcher; + private final ContractOfferFetcher contractOfferFetcher; + private final BrokerEventLogger brokerEventLogger; + + /** + * Updates single connector. + * + * @param connectorEndpoint connector endpoint + */ + public void updateConnector(String connectorEndpoint) { + // TODO implement + throw new IllegalArgumentException("Not yet implemented"); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java new file mode 100644 index 000000000..87c1dc4a8 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; + +import java.util.List; + +public class ContractOfferFetcher { + + /** + * Fetches Connector contract offers + * + * @param connectorEndpoint connector endpoint + * @return updated connector db row + */ + public List fetchContractOffers(String connectorEndpoint) { + // TODO implement + throw new IllegalArgumentException("Not yet implemented"); + } +} diff --git a/gradle.properties b/gradle.properties index 8ffeb6ed9..18c20a97d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,8 @@ sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions sovityEdcExtensionsVersion=3.1.1-SNAPSHOT -sovityEdcExtensionsGroup=de.sovity.edc.ext +sovityEdcExtensionGroup=de.sovity.edc.ext +sovityEdcGroup=de.sovity.edc # Eclipse EDC edcGroup=org.eclipse.edc From f0fc88ba8e5c436830949bd2ba7451cefe24374c Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 15 May 2023 09:31:16 +0200 Subject: [PATCH 012/295] chore document development requirements (#34) --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1122cc86e..9e9a61521 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,12 @@ Broker Backend & EDC Extensions.

Table of Contents
  1. About The Project
  2. +
  3. Requirements
  4. License
  5. Contact
- - ## About The Project [Eclipse Dataspace Components](https://github.com/eclipse-edc) (EDC) is a framework @@ -47,7 +46,19 @@ This IDS Broker is written on basis of the EDC and should be used in tandem with

(back to top)

- +## Requirements + +For development, access to the GitHub Maven Registry is required. + +To access the GitHub Maven Registry you need to provide the following properties, e.g. by providing +a `~/.gradle/gradle.properties`. + +```properties +gpr.user={your github username} +gpr.key={your github pat with packages.read} +``` + +

(back to top)

## License @@ -55,8 +66,6 @@ Distributed under the Apache 2.0 License. See `LICENSE` for more information.

(back to top)

- - ## Contact contact@sovity.de From f5ac64bd7f5c0993ba49761ba4be02b436ff3df2 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 23 May 2023 09:14:13 +0200 Subject: [PATCH 013/295] PostgreSQL + JOOQ + Testcontainers (#39) * feat postgres + JooQ + Testcontainers --- .../build-and-publish-connector-images.yml | 23 +- connector/Dockerfile | 8 + connector/build.gradle.kts | 6 +- docs/dev/checkstyle/checkstyle-config.xml | 36 +- .../README.md | 31 + .../build.gradle.kts | 197 +++++++ .../brokerserver/db/DataSourceFactory.java | 51 ++ .../brokerserver/db/DslContextFactory.java | 48 ++ .../ext/brokerserver/db/FlywayFactory.java | 42 ++ .../ext/brokerserver/db/FlywayMigrator.java | 91 +++ .../db/PostgresFlywayExtension.java | 58 ++ .../brokerserver/db/utils/ConfigUtils.java | 30 + .../db/utils/JdbcCredentials.java | 39 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../main/resources/db/migration/V1__MS8.sql | 529 ++++++++++++++++++ .../V2__Broker_Server_Initial_DB_Model.sql | 16 + extensions/broker-server/build.gradle.kts | 22 +- .../brokerserver/BrokerServerExtension.java | 18 +- .../BrokerServerExtensionContextBuilder.java | 14 +- .../dao/models/ConnectorOnlineStatus.java | 20 - .../dao/models/ConnectorRecord.java | 47 -- .../dao/models/LogEventRecord.java | 2 +- .../dao/stores/ConnectorQueries.java | 28 + .../dao/stores/ConnectorStore.java | 39 -- .../services/BrokerEventLogger.java | 4 +- .../services/BrokerServerInitializer.java | 66 ++- .../services/api/ConnectorApiService.java | 32 +- .../ConnectorSelfDescriptionFetcher.java | 9 +- .../ext/brokerserver/ConnectorApiTest.java | 51 ++ .../edc/ext/brokerserver/TestUtils.java | 75 +++ .../edc/ext/brokerserver/db/TestDatabase.java | 26 + .../brokerserver/db/TestDatabaseFactory.java | 35 ++ .../brokerserver/db/TestDatabaseViaEnv.java | 59 ++ .../db/TestDatabaseViaTestcontainers.java | 46 ++ gradle.properties | 3 +- settings.gradle.kts | 1 + 36 files changed, 1611 insertions(+), 192 deletions(-) create mode 100644 extensions/broker-server-postgres-flyway-jooq/README.md create mode 100644 extensions/broker-server-postgres-flyway-jooq/build.gradle.kts create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java diff --git a/.github/workflows/build-and-publish-connector-images.yml b/.github/workflows/build-and-publish-connector-images.yml index 38d2a6739..794a7822d 100644 --- a/.github/workflows/build-and-publish-connector-images.yml +++ b/.github/workflows/build-and-publish-connector-images.yml @@ -29,7 +29,7 @@ jobs: "imageName": "broker-server-ce", "title": "Broker Server (Community Edition)", "description": "EDC IDS Broker Server. Contains DB extensions and requires dataspace credentials to join an existing dataspace.", - "buildArgs": "-Pfs-vault -Pdmgmt-api-key -Ppostgres-flyway -Poauth2" + "buildArgs": "-Pdmgmt-api-key -Pfs-vault -Poauth2" } ] timeout-minutes: 30 @@ -37,17 +37,30 @@ jobs: contents: read packages: write + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: edc + POSTGRES_PASSWORD: edc + POSTGRES_DB: edc + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout repository uses: actions/checkout@v3 - - name: Log in to the Container registry uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v4 @@ -74,7 +87,11 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + network: host build-args: | USERNAME=${{ github.actor }} TOKEN=${{ secrets.GITHUB_TOKEN }} BUILD_ARGS=${{ matrix.imageVariants.buildArgs }} + TEST_POSTGRES_JDBC_URL=jdbc:postgresql://localhost:5432/edc + TEST_POSTGRES_JDBC_USER=edc + TEST_POSTGRES_JDBC_PASSWORD=edc diff --git a/connector/Dockerfile b/connector/Dockerfile index 2ef8eba55..b353bd390 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -3,10 +3,18 @@ FROM gradle:7.6.0-jdk17 AS build ARG USERNAME ARG TOKEN ARG BUILD_ARGS +ARG TEST_POSTGRES_JDBC_URL +ARG TEST_POSTGRES_JDBC_USER +ARG TEST_POSTGRES_JDBC_PASSWORD ENV USERNAME=$USERNAME ENV TOKEN=$TOKEN +ENV SKIP_TESTCONTAINERS=true +ENV TEST_POSTGRES_JDBC_URL=$TEST_POSTGRES_JDBC_URL +ENV TEST_POSTGRES_JDBC_USER=$TEST_POSTGRES_JDBC_USER +ENV TEST_POSTGRES_JDBC_PASSWORD=$TEST_POSTGRES_JDBC_PASSWORD + COPY --chown=gradle:gradle . /home/gradle/project/ WORKDIR /home/gradle/project/ RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle build --no-daemon $BUILD_ARGS diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index c00c858ed..2fce034a4 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -23,10 +23,8 @@ dependencies { // JDK Logger implementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") - // Optional: PostgreSQL + Flyway - if (project.hasProperty("postgres-flyway")) { - implementation("${sovityEdcExtensionGroup}:postgres-flyway:${sovityEdcExtensionsVersion}") - } + // Broker Server + PostgreSQL + Flyway + implementation(project(":extensions:broker-server")) // Optional: Connector-To-Connector IAM if (project.hasProperty("oauth2")) { diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml index 9137f854c..4d9976e07 100644 --- a/docs/dev/checkstyle/checkstyle-config.xml +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -16,16 +16,16 @@ Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov. --> - + - + - + @@ -51,7 +51,8 @@ - + @@ -222,7 +223,8 @@ - + @@ -278,10 +280,8 @@ - - @@ -334,10 +334,6 @@ value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/> - - - - @@ -351,22 +347,6 @@ - + default="checkstyle-xpath-suppressions.xml"/> diff --git a/extensions/broker-server-postgres-flyway-jooq/README.md b/extensions/broker-server-postgres-flyway-jooq/README.md new file mode 100644 index 000000000..3c25ebe04 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/README.md @@ -0,0 +1,31 @@ + +
+
+ + Logo + + +

Broker Server:
PostgreSQL + Flyway + JooQ

+ +

+ Report Bug + · + Request Feature +

+
+ +## About this Extension Package + +This module contains: + +- An EDC Extension migrating the db schema on startup. +- The entire Broker Server DB Schema as JooQ generated code. +- A `DslContextFactory` to quickly start using the JooQ generated code. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts new file mode 100644 index 000000000..50070881a --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -0,0 +1,197 @@ +import org.flywaydb.gradle.task.FlywayCleanTask +import org.flywaydb.gradle.task.FlywayMigrateTask +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.containers.PostgreSQLContainer + +val jooqDbType = "org.jooq.meta.postgres.PostgresDatabase" +val jdbcDriver = "org.postgresql.Driver" +val postgresContainer = "postgres:11-alpine" + +val migrationsDir = "src/main/resources/db/migration" +val jooqTargetPackage = "de.sovity.edc.ext.brokerserver.db.jooq" +val jooqTargetSourceRoot = "build/generated/jooq" + +val jooqTargetDir = jooqTargetSourceRoot + "/" + jooqTargetPackage.replace(".", "/") +val flywayMigration = configurations.create("flywayMigration") + +val edcVersion: String by project +val edcGroup: String by project +val flywayVersion: String by project +val postgresVersion: String by project + +buildscript { + dependencies { + classpath("org.testcontainers:postgresql:1.17.6") + } +} + +plugins { + id("org.flywaydb.flyway") version "9.3.0" + id("nu.studer.jooq") version "7.1.1" + `java-library` + `maven-publish` +} + +dependencies { + api("org.jooq:jooq:3.16.4") + jooqGenerator("org.postgresql:postgresql:42.5.0") + flywayMigration("org.postgresql:postgresql:42.5.0") + + annotationProcessor("org.projectlombok:lombok:1.18.26") + compileOnly("org.projectlombok:lombok:1.18.26") + implementation("org.apache.commons:commons-lang3:3.12.0") + + implementation("${edcGroup}:core-spi:${edcVersion}") + implementation("${edcGroup}:sql-core:${edcVersion}") + + // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) + implementation("${edcGroup}:control-plane-sql:${edcVersion}") + implementation("${edcGroup}:data-plane-instance-store-sql:${edcVersion}") + implementation("${edcGroup}:sql-pool-apache-commons:${edcVersion}") + implementation("${edcGroup}:transaction-local:${edcVersion}") + implementation("org.postgresql:postgresql:${postgresVersion}") + + implementation("org.flywaydb:flyway-core:${flywayVersion}") + + testImplementation("${edcGroup}:junit:${edcVersion}") +} + +sourceSets { + main { + java { + srcDirs.add(File(jooqTargetSourceRoot)) + } + } +} + + +val skipTestcontainersEnvVarName = "SKIP_TESTCONTAINERS" +val jdbcUrlEnvVarName = "TEST_POSTGRES_JDBC_URL" +val jdbcUserEnvVarName = "TEST_POSTGRES_JDBC_USER" +val jdbcPasswordEnvVarName = "TEST_POSTGRES_JDBC_PASSWORD" +var container: JdbcDatabaseContainer<*>? = null + +fun isTestcontainersEnabled(): Boolean { + return System.getenv()[skipTestcontainersEnvVarName] != "true" +} + +fun jdbcUrl(): String { + return container?.jdbcUrl + ?: System.getenv()[jdbcUrlEnvVarName] + ?: error("Need $jdbcUrlEnvVarName since $skipTestcontainersEnvVarName=true") +} + +fun jdbcUser(): String { + return container?.username + ?: System.getenv()[jdbcUserEnvVarName] + ?: error("Need $jdbcUserEnvVarName since $skipTestcontainersEnvVarName=true") +} + +fun jdbcPassword(): String { + return container?.password + ?: System.getenv()[jdbcPasswordEnvVarName] + ?: error("Need $jdbcPasswordEnvVarName since $skipTestcontainersEnvVarName=true") +} + + +tasks.register("startTestcontainer") { + doLast { + if (isTestcontainersEnabled()) { + container = PostgreSQLContainer(postgresContainer) + container!!.start() + gradle.buildFinished { + container?.stop() + } + } + } +} + +flyway { + driver = jdbcDriver + schemas = arrayOf("public") + + cleanDisabled = false + cleanOnValidationError = true + baselineOnMigrate = true + locations = arrayOf("filesystem:${migrationsDir}") + configurations = arrayOf("flywayMigration") +} + +tasks.withType { + doFirst { + require(this is FlywayCleanTask) + url = jdbcUrl() + user = jdbcUser() + password = jdbcPassword() + } +} + +tasks.withType { + dependsOn.add("startTestcontainer") + doFirst { + require(this is FlywayMigrateTask) + url = jdbcUrl() + user = jdbcUser() + password = jdbcPassword() + } +} + +jooq { + configurations { + create("main") { + jooqConfiguration.apply { + generator.apply { + database.apply { + name = jooqDbType + excludes = "(.*)flyway_schema_history(.*)" + inputSchema = flyway.schemas[0] + } + generate.apply { + isRecords = true + isRelations = true + } + target.apply { + packageName = jooqTargetPackage + directory = jooqTargetDir + } + } + } + } + } +} + +tasks.withType { + dependsOn.add("flywayMigrate") + inputs.files(fileTree(migrationsDir)) + .withPropertyName("migrations") + .withPathSensitivity(PathSensitivity.RELATIVE) + allInputsDeclared.set(true) + outputs.cacheIf { true } + doFirst { + require(this is nu.studer.gradle.jooq.JooqGenerate) + + val jooqConfiguration = nu.studer.gradle.jooq.JooqGenerate::class.java.getDeclaredField("jooqConfiguration") + .also { it.isAccessible = true }.get(this) as org.jooq.meta.jaxb.Configuration + + jooqConfiguration.jdbc.apply { + url = jdbcUrl() + user = jdbcUser() + password = jdbcPassword() + } + } + doLast { + container?.stop() + } +} + + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java new file mode 100644 index 000000000..75cefd147 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import de.sovity.edc.ext.brokerserver.db.utils.JdbcCredentials; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.sql.datasource.ConnectionFactoryDataSource; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import javax.sql.DataSource; + +/** + * Create {@link DataSource}s from EDC Config. + */ +@RequiredArgsConstructor +public class DataSourceFactory { + private final Config config; + + public DataSource newDataSource() { + var jdbcCredentials = JdbcCredentials.fromConfig(config); + return new ConnectionFactoryDataSource(() -> newConnection(jdbcCredentials)); + } + + private Connection newConnection(JdbcCredentials jdbcCredentials) { + try { + return DriverManager.getConnection( + jdbcCredentials.jdbcUrl(), + jdbcCredentials.jdbcUser(), + jdbcCredentials.jdbcPassword() + ); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java new file mode 100644 index 000000000..8dd33c0ce --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java @@ -0,0 +1,48 @@ +package de.sovity.edc.ext.brokerserver.db; + +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; + +import java.util.function.Consumer; +import java.util.function.Function; +import javax.sql.DataSource; + +/** + * Quickly launch {@link org.jooq.DSLContext}s from EDC configuration. + */ +@RequiredArgsConstructor +public class DslContextFactory { + private final DataSource dataSource; + + /** + * Create new {@link DSLContext} for querying DB. + * + * @return new {@link DSLContext} + */ + public DSLContext newDslContext() { + return DSL.using(dataSource, SQLDialect.POSTGRES); + } + + /** + * Utility method for when the {@link DSLContext} will be used only for a single transaction. + *
+ * An example would be a REST request. + * + * @param return type + * @return new {@link DSLContext} + opened transaction + */ + public R transaction(Function function) { + return newDslContext().transactionResult(transaction -> function.apply(transaction.dsl())); + } + + /** + * Utility method for when the {@link DSLContext} will be used only for a single transaction. + *
+ * An example would be a REST request. + */ + public void transaction(Consumer function) { + newDslContext().transaction(transaction -> function.accept(transaction.dsl())); + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java new file mode 100644 index 000000000..9b3bb7e3d --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import lombok.RequiredArgsConstructor; +import org.flywaydb.core.Flyway; + +import javax.sql.DataSource; + +/** + * Quickly launch {@link Flyway} from EDC Config + */ +@RequiredArgsConstructor +public class FlywayFactory { + + /** + * Configure and launch {@link Flyway}. + * + * @param dataSource data source + * @return {@link Flyway} + */ + public Flyway setupFlyway(DataSource dataSource) { + return Flyway.configure() + .baselineOnMigrate(true) + .dataSource(dataSource) + .table("flyway_schema_history") + .locations("classpath:db/migration") + .load(); + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java new file mode 100644 index 000000000..a06768a0d --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.system.configuration.Config; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; + + +/** + * Custom flyway migration logic and logging. + */ +@RequiredArgsConstructor +public class FlywayMigrator { + private final Flyway flyway; + private final Config config; + private final Monitor monitor; + + /** + * Run migrations and potentially run flyway repair + */ + public void migrateAndRepair() { + try { + migrate(); + } catch (FlywayException e) { + if (isFlywayRepairEnabled()) { + try { + repair(); + migrate(); + } catch (FlywayException e2) { + throw migrationFailed(e2); + } + } else { + throw migrationFailed(e); + } + } + } + + private void migrate() { + var migrateResult = flyway.migrate(); + if (migrateResult.migrationsExecuted > 0) { + monitor.info(String.format( + "Successfully migrated database from version %s to version %s", + migrateResult.initialSchemaVersion, + migrateResult.targetSchemaVersion + )); + } else { + monitor.info(String.format( + "No migration necessary. Current version is %s", + migrateResult.initialSchemaVersion + )); + } + } + + private void repair() { + var repairResult = flyway.repair(); + if (!repairResult.repairActions.isEmpty()) { + var repairActions = String.join(", ", repairResult.repairActions); + monitor.info(String.format("Flyway Repair actions: %s", repairActions)); + } + + if (!repairResult.warnings.isEmpty()) { + var warnings = String.join(", ", repairResult.warnings); + throw new EdcPersistenceException(String.format("Flyway Repair failed: %s", warnings)); + } + } + + private boolean isFlywayRepairEnabled() { + return config.getBoolean(PostgresFlywayExtension.FLYWAY_REPAIR, true); + } + + + private EdcPersistenceException migrationFailed(FlywayException e) { + return new EdcPersistenceException("Flyway migration failed", e); + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java new file mode 100644 index 000000000..cc5baa112 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import org.eclipse.edc.connector.dataplane.selector.store.sql.schema.DataPlaneInstanceStatements; +import org.eclipse.edc.connector.dataplane.selector.store.sql.schema.postgres.PostgresDataPlaneInstanceStatements; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +public class PostgresFlywayExtension implements ServiceExtension { + @Setting(required = true) + public static final String JDBC_URL = "edc.datasource.default.url"; + @Setting(required = true) + public static final String JDBC_USER = "edc.datasource.default.jdbcuser"; + @Setting(required = true) + public static final String JDBC_PASSWORD = "edc.datasource.default.jdbcpassword"; + @Setting + public static final String FLYWAY_REPAIR = "edc.flyway.repair"; + + @Provider + public DataPlaneInstanceStatements dataPlaneInstanceStatements() { + return new PostgresDataPlaneInstanceStatements(); + } + + @Override + public String name() { + return "Postgres Flyway Extension (Broker Server)"; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var config = context.getConfig(); + var monitor = context.getMonitor(); + + var dataSourceFactory = new DataSourceFactory(config); + var dataSource = dataSourceFactory.newDataSource(); + + var flywayFactory = new FlywayFactory(); + var flyway = flywayFactory.setupFlyway(dataSource); + var flywayMigrator = new FlywayMigrator(flyway, config, monitor); + flywayMigrator.migrateAndRepair(); + } + +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java new file mode 100644 index 000000000..0efef4996 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.Validate; +import org.eclipse.edc.spi.system.configuration.Config; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ConfigUtils { + + public static String getRequiredStringProperty(Config config, String name) { + String value = config.getString(name, ""); + Validate.notBlank(value, "EDC Property '%s' is required".formatted(name)); + return value; + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java new file mode 100644 index 000000000..e6d664ce2 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db.utils; + +import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; +import org.eclipse.edc.spi.system.configuration.Config; + +/** + * JDBC Credentials + * + * @param jdbcUrl JDBC URL without credentials + * @param jdbcUser JDBC User + * @param jdbcPassword JDBC Password + */ +public record JdbcCredentials( + String jdbcUrl, + String jdbcUser, + String jdbcPassword +) { + public static JdbcCredentials fromConfig(Config config) { + return new JdbcCredentials( + ConfigUtils.getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_URL), + ConfigUtils.getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_USER), + ConfigUtils.getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_PASSWORD) + ); + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..1f8b84893 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension \ No newline at end of file diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql new file mode 100644 index 000000000..9c5ff65f3 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql @@ -0,0 +1,529 @@ +-- +-- Copyright (c) 2022 Daimler TSS GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Daimler TSS GmbH - Initial SQL Query +-- + +-- THIS SCHEMA HAS BEEN WRITTEN AND TESTED ONLY FOR POSTGRES + +-- table: edc_asset +CREATE TABLE IF NOT EXISTS edc_asset +( + asset_id VARCHAR NOT NULL, + PRIMARY KEY (asset_id) +); + +-- table: edc_asset_dataaddress +CREATE TABLE IF NOT EXISTS edc_asset_dataaddress +( + asset_id_fk VARCHAR NOT NULL, + properties JSON NOT NULL, + PRIMARY KEY (asset_id_fk), + FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE +); +COMMENT ON COLUMN edc_asset_dataaddress.properties IS 'DataAddress properties serialized as JSON'; + +-- table: edc_asset_property +CREATE TABLE IF NOT EXISTS edc_asset_property +( + asset_id_fk VARCHAR NOT NULL, + property_name VARCHAR NOT NULL, + property_value VARCHAR NOT NULL, + property_type VARCHAR NOT NULL, + PRIMARY KEY (asset_id_fk, property_name), + FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE +); + +COMMENT ON COLUMN edc_asset_property.property_name IS + 'Asset property key'; +COMMENT ON COLUMN edc_asset_property.property_value IS + 'Asset property value'; +COMMENT ON COLUMN edc_asset_property.property_type IS + 'Asset property class name'; + +CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value + ON edc_asset_property (property_name, property_value); + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_asset + ADD created_at BIGINT; + +UPDATE edc_asset +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_asset + ALTER COLUMN created_at SET NOT NULL; + +-- +-- Copyright (c) 2022 Daimler TSS GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Daimler TSS GmbH - Initial SQL Query +-- Microsoft Corporation - refactoring +-- + +-- table: edc_contract_definitions +-- only intended for and tested with H2 and Postgres! +CREATE TABLE IF NOT EXISTS edc_contract_definitions +( + contract_definition_id VARCHAR NOT NULL, + access_policy_id VARCHAR NOT NULL, + contract_policy_id VARCHAR NOT NULL, + selector_expression JSON NOT NULL, + PRIMARY KEY (contract_definition_id) +); + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_definitions + ADD created_at BIGINT; + +UPDATE edc_contract_definitions +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_contract_definitions + ALTER COLUMN created_at SET NOT NULL; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- + +ALTER TABLE edc_contract_definitions + ADD validity BIGINT; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_contract_definitions +SET validity=60 * 60 * 24 * 365 +WHERE validity IS NULL; + +-- Statements are designed for and tested with Postgres only! + +CREATE TABLE IF NOT EXISTS edc_lease +( + leased_by VARCHAR NOT NULL, + leased_at BIGINT, + lease_duration INTEGER DEFAULT 60000 NOT NULL, + lease_id VARCHAR NOT NULL + CONSTRAINT lease_pk + PRIMARY KEY +); + +COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; + +COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; + + +CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex + ON edc_lease (lease_id); + + + +CREATE TABLE IF NOT EXISTS edc_contract_agreement +( + agr_id VARCHAR NOT NULL + CONSTRAINT contract_agreement_pk + PRIMARY KEY, + provider_agent_id VARCHAR, + consumer_agent_id VARCHAR, + signing_date BIGINT, + start_date BIGINT, + end_date INTEGER, + asset_id VARCHAR NOT NULL, + policy JSON +); + + +CREATE TABLE IF NOT EXISTS edc_contract_negotiation +( + id VARCHAR NOT NULL + CONSTRAINT contract_negotiation_pk + PRIMARY KEY, + correlation_id VARCHAR, + counterparty_id VARCHAR NOT NULL, + counterparty_address VARCHAR NOT NULL, + protocol VARCHAR DEFAULT 'ids-multipart'::CHARACTER VARYING NOT NULL, + type INTEGER DEFAULT 0 NOT NULL, + state INTEGER DEFAULT 0 NOT NULL, + state_count INTEGER DEFAULT 0, + state_timestamp BIGINT, + error_detail VARCHAR, + agreement_id VARCHAR + CONSTRAINT contract_negotiation_contract_agreement_id_fk + REFERENCES edc_contract_agreement, + contract_offers JSON, + trace_context JSON, + lease_id VARCHAR + CONSTRAINT contract_negotiation_lease_lease_id_fk + REFERENCES edc_lease + ON DELETE SET NULL, + CONSTRAINT provider_correlation_id CHECK (type = '0' OR correlation_id IS NOT NULL) +); + +COMMENT ON COLUMN edc_contract_negotiation.agreement_id IS 'ContractAgreement serialized as JSON'; + +COMMENT ON COLUMN edc_contract_negotiation.contract_offers IS 'List serialized as JSON'; + +COMMENT ON COLUMN edc_contract_negotiation.trace_context IS 'Map serialized as JSON'; + + +CREATE INDEX IF NOT EXISTS contract_negotiation_correlationid_index + ON edc_contract_negotiation (correlation_id); + +CREATE UNIQUE INDEX IF NOT EXISTS contract_negotiation_id_uindex + ON edc_contract_negotiation (id); + +CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex + ON edc_contract_agreement (agr_id); + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_negotiation + ADD created_at BIGINT, + ADD updated_at BIGINT; + +UPDATE edc_contract_negotiation +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; +UPDATE edc_contract_negotiation +SET updated_at=created_at; + +ALTER TABLE edc_contract_negotiation + ALTER COLUMN created_at SET NOT NULL; +ALTER TABLE edc_contract_negotiation + ALTER COLUMN updated_at SET NOT NULL; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +UPDATE edc_contract_negotiation +SET contract_offers = co.contract_offers_edited +FROM (SELECT cn.id, + jsonb_agg( + jsonb_set( + jsonb_set( + elems, + '{contractStart}', + to_json(to_char(to_timestamp(created_at / 1000) AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ), + '{contractEnd}', + to_json(to_char(to_timestamp((created_at / 1000) + 60 * 60 * 24 * 365) AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ) + )::json as contract_offers_edited + FROM edc_contract_negotiation cn, + jsonb_array_elements(cn.contract_offers::jsonb) elems + GROUP BY cn.id) co +WHERE edc_contract_negotiation.id = co.id; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - initial API and implementation for DataplaneInstances +-- +-- +CREATE TABLE IF NOT EXISTS edc_data_plane_instance +( + id VARCHAR NOT NULL, + data JSON NOT NULL, + PRIMARY KEY (id) +); + +-- +-- Copyright (c) 2022 ZF Friedrichshafen AG +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- ZF Friedrichshafen AG - Initial SQL Query +-- + +-- Statements are designed for and tested with Postgres only! + +-- table: edc_policydefinitions +CREATE TABLE IF NOT EXISTS edc_policydefinitions +( + policy_id VARCHAR NOT NULL, + permissions JSON, + prohibitions JSON, + duties JSON, + extensible_properties JSON, + inherits_from VARCHAR, + assigner VARCHAR, + assignee VARCHAR, + target VARCHAR, + policy_type VARCHAR NOT NULL, + PRIMARY KEY (policy_id) +); + +COMMENT ON COLUMN edc_policydefinitions.permissions IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.prohibitions IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.duties IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.extensible_properties IS 'Java Map serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.policy_type IS 'Java PolicyType serialized as JSON'; + +CREATE UNIQUE INDEX IF NOT EXISTS edc_policydefinitions_id_uindex + ON edc_policydefinitions (policy_id); + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_policydefinitions + ADD created_at BIGINT; + +UPDATE edc_policydefinitions +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_policydefinitions + ALTER COLUMN created_at SET NOT NULL; + +-- Statements are designed for and tested with Postgres only! + +CREATE TABLE IF NOT EXISTS edc_lease +( + leased_by VARCHAR NOT NULL, + leased_at BIGINT, + lease_duration INTEGER NOT NULL, + lease_id VARCHAR NOT NULL + CONSTRAINT lease_pk + PRIMARY KEY +); + +COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; + +COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; + +CREATE TABLE IF NOT EXISTS edc_transfer_process +( + transferprocess_id VARCHAR NOT NULL + CONSTRAINT transfer_process_pk + PRIMARY KEY, + type VARCHAR NOT NULL, + state INTEGER NOT NULL, + state_count INTEGER DEFAULT 0 NOT NULL, + state_time_stamp BIGINT, + created_time_stamp BIGINT, + trace_context JSON, + error_detail VARCHAR, + resource_manifest JSON, + provisioned_resource_set JSON, + content_data_address JSON, + deprovisioned_resources JSON, + lease_id VARCHAR + CONSTRAINT transfer_process_lease_lease_id_fk + REFERENCES edc_lease + ON DELETE SET NULL +); + +COMMENT ON COLUMN edc_transfer_process.trace_context IS 'Java Map serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.resource_manifest IS 'java ResourceManifest serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.provisioned_resource_set IS 'ProvisionedResourceSet serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.content_data_address IS 'DataAddress serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.deprovisioned_resources IS 'List of deprovisioned resources, serialized as JSON'; + + +CREATE UNIQUE INDEX IF NOT EXISTS transfer_process_id_uindex + ON edc_transfer_process (transferprocess_id); + +CREATE TABLE IF NOT EXISTS edc_data_request +( + datarequest_id VARCHAR NOT NULL + CONSTRAINT data_request_pk + PRIMARY KEY, + process_id VARCHAR NOT NULL, + connector_address VARCHAR NOT NULL, + protocol VARCHAR NOT NULL, + connector_id VARCHAR, + asset_id VARCHAR NOT NULL, + contract_id VARCHAR NOT NULL, + data_destination JSON NOT NULL, + managed_resources BOOLEAN DEFAULT TRUE, + properties JSON, + transfer_type JSON, + transfer_process_id VARCHAR NOT NULL + CONSTRAINT data_request_transfer_process_id_fk + REFERENCES edc_transfer_process + ON UPDATE RESTRICT ON DELETE CASCADE +); + +COMMENT ON COLUMN edc_data_request.data_destination IS 'DataAddress serialized as JSON'; + +COMMENT ON COLUMN edc_data_request.properties IS 'java Map serialized as JSON'; + +COMMENT ON COLUMN edc_data_request.transfer_type IS 'TransferType serialized as JSON'; + + +CREATE UNIQUE INDEX IF NOT EXISTS data_request_id_uindex + ON edc_data_request (datarequest_id); + +CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex + ON edc_lease (lease_id); + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_transfer_process + RENAME COLUMN created_time_stamp TO created_at; + +UPDATE edc_transfer_process +SET created_at = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 +WHERE created_at = NULL; + +ALTER TABLE edc_transfer_process + ADD updated_at BIGINT; + +UPDATE edc_transfer_process +SET updated_at=created_at; + +ALTER TABLE edc_transfer_process + ALTER COLUMN updated_at SET NOT NULL; +ALTER TABLE edc_transfer_process + ALTER COLUMN created_at SET NOT NULL; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +ALTER TABLE edc_transfer_process + ADD transferprocess_properties JSON; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_transfer_process +SET transferprocess_properties = '{}'::json +WHERE transferprocess_properties IS NULL; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql new file mode 100644 index 000000000..9d3318b16 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -0,0 +1,16 @@ +create type connector_online_status as enum ('ONLINE', 'OFFLINE'); + +create table connector +( + endpoint varchar(512) not null, + connector_id varchar(512) not null, + ids_id varchar(512) not null, + title varchar(512), + description text, + last_update timestamp with time zone, + offline_since timestamp with time zone, + created_at timestamp with time zone not null, + online_status connector_online_status not null, + + PRIMARY KEY (endpoint) +); \ No newline at end of file diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index ac316c091..b6e591a91 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -10,6 +10,8 @@ val assertj: String by project val okHttpVersion: String by project val sovityEdcGroup: String by project val sovityEdcExtensionsVersion: String by project +val restAssured: String by project +val testcontainersVersion: String by project dependencies { annotationProcessor("org.projectlombok:lombok:1.18.26") @@ -23,23 +25,33 @@ dependencies { implementation("${edcGroup}:ids-api-configuration:${edcVersion}") implementation("${edcGroup}:ids-jsonld-serdes:${edcVersion}") + api(project(":extensions:broker-server-postgres-flyway-jooq")) api("${sovityEdcGroup}:wrapper-broker-api:${sovityEdcExtensionsVersion}") implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") + testAnnotationProcessor("org.projectlombok:lombok:1.18.26") + testCompileOnly("org.projectlombok:lombok:1.18.26") testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-params:${jupiterVersion}") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}") + testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation("${edcGroup}:http:${edcVersion}") + testImplementation("${edcGroup}:iam-mock:${edcVersion}") + testImplementation("io.rest-assured:rest-assured:${restAssured}") + testImplementation("${sovityEdcGroup}:client:${sovityEdcExtensionsVersion}") + testImplementation("org.testcontainers:testcontainers:${testcontainersVersion}") + testImplementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") + testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") } tasks.getByName("test") { useJUnitPlatform() } -tasks.register("prepareKotlinBuildScriptModel"){} +tasks.register("prepareKotlinBuildScriptModel") {} publishing { publications { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 6a863b893..fe4e00b43 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -17,6 +17,7 @@ import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; @@ -28,12 +29,20 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String KNOWN_CONNECTORS = "edc.brokerserver.known.connectors"; + @Inject + private AssetIndex assetIndex; // ensures db is initialized before + @Inject private ManagementApiConfiguration managementApiConfiguration; @Inject private WebService webService; + /** + * Manual Dependency Injection + */ + private BrokerServerExtensionContext services; + @Override public String name() { return EXTENSION_NAME; @@ -41,10 +50,13 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var services = BrokerServerExtensionContextBuilder.buildContext(context.getConfig()); - services.brokerServerInitializer().initializeConnectorList(); - + services = BrokerServerExtensionContextBuilder.buildContext(context.getConfig()); String managementApiGroup = managementApiConfiguration.getContextAlias(); webService.registerResource(managementApiGroup, services.brokerServerResource()); } + + @Override + public void start() { + services.brokerServerInitializer().onStartup(); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index b4ca84608..a049cffef 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -14,8 +14,10 @@ package de.sovity.edc.ext.brokerserver; -import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; import de.sovity.edc.ext.brokerserver.dao.stores.ContractOfferStore; +import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; @@ -37,11 +39,14 @@ public class BrokerServerExtensionContextBuilder { public static BrokerServerExtensionContext buildContext(Config config) { // Dao - var connectorStore = new ConnectorStore(); + var dataSourceFactory = new DataSourceFactory(config); + var dataSource = dataSourceFactory.newDataSource(); + var dslContextFactory = new DslContextFactory(dataSource); + var connectorQueries = new ConnectorQueries(); var contractOfferStore = new ContractOfferStore(); // Services - var brokerServerInitializer = new BrokerServerInitializer(connectorStore, config); + var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config); // UI Capabilities var paginationMetadataUtils = new PaginationMetadataUtils(); @@ -50,7 +55,8 @@ public static BrokerServerExtensionContext buildContext(Config config) { paginationMetadataUtils ); var connectorApiService = new ConnectorApiService( - connectorStore, + dslContextFactory, + connectorQueries, paginationMetadataUtils ); var brokerServerResource = new BrokerServerResourceImpl(connectorApiService, catalogApiService); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java deleted file mode 100644 index a0840ce8c..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorOnlineStatus.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.models; - -public enum ConnectorOnlineStatus { - ONLINE, - OFFLINE -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java deleted file mode 100644 index 0b6445a69..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorRecord.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; - -/** - * Connector Database Row that can be inserted or updated. - *

- * Represents metadata for another connector in the dataspace. - */ -@Getter -@ToString -@Builder(toBuilder = true) -@EqualsAndHashCode(of = "id") -@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) -@FieldDefaults(makeFinal = true, level = lombok.AccessLevel.PRIVATE) -public class ConnectorRecord { - String id; - String idsId; - String title; - String description; - String endpoint; - OffsetDateTime lastUpdate; - OffsetDateTime offlineSince; - OffsetDateTime createdAt; - ConnectorOnlineStatus onlineStatus; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java index 660b44104..ea9cf2789 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java @@ -60,7 +60,7 @@ public class LogEventRecord { /** * Connector reference, if applicable */ - String connectorId; + String connectorEndpoint; /** * Contract Offer reference, if applicable diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java new file mode 100644 index 000000000..8480712b9 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.stores; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import org.jooq.DSLContext; + +import java.util.stream.Stream; + +public class ConnectorQueries { + + public Stream findAll(DSLContext dslContext) { + return dslContext.selectFrom(Tables.CONNECTOR).stream(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java deleted file mode 100644 index 622604be1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorStore.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.stores; - -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; -import org.apache.commons.lang3.Validate; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -public class ConnectorStore { - private final Map connectorsById = new HashMap<>(); - - public Stream findAll() { - return connectorsById.values().stream(); - } - - public ConnectorRecord findById(String connectorId) { - return connectorsById.get(connectorId); - } - - public ConnectorRecord save(ConnectorRecord connector) { - Validate.notBlank(connector.getId(), "Need Connector ID"); - return connectorsById.put(connector.getId(), connector); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java index dd6e3e1ce..3cb33f44a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java @@ -14,11 +14,11 @@ package de.sovity.edc.ext.brokerserver.services; -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; import de.sovity.edc.ext.brokerserver.dao.models.LogEventRecord; import de.sovity.edc.ext.brokerserver.dao.models.LogEventStatus; import de.sovity.edc.ext.brokerserver.dao.models.LogEventType; import de.sovity.edc.ext.brokerserver.dao.stores.LogEventStore; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import lombok.RequiredArgsConstructor; import java.time.OffsetDateTime; @@ -32,7 +32,7 @@ public class BrokerEventLogger { public void logConnectorUpdate(ConnectorRecord connector, LogEventStatus outcome) { var logEntry = LogEventRecord.builder() - .connectorId(connector.getId()) + .connectorEndpoint(connector.getEndpoint()) .userMessage(getConnectorUpdatedMessage(outcome)) .type(LogEventType.CONNECTOR_UPDATED) .createdAt(OffsetDateTime.now()) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java index 2fd35ebd8..c9fbc06ad 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java @@ -15,33 +15,69 @@ package de.sovity.edc.ext.brokerserver.services; import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.system.configuration.Config; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import java.net.URI; import java.time.OffsetDateTime; import java.util.Arrays; @RequiredArgsConstructor public class BrokerServerInitializer { - private final ConnectorStore connectorStore; + private final DslContextFactory dslContextFactory; private final Config config; - public void initializeConnectorList() { - var knownConnectors = config.getString(BrokerServerExtension.KNOWN_CONNECTORS).split(","); + public void onStartup() { + dslContextFactory.transaction(this::initializeConnectorList); + } + + private void initializeConnectorList(DSLContext dsl) { + var endpoints = config.getString(BrokerServerExtension.KNOWN_CONNECTORS).split(","); + var connectorRecords = Arrays.stream(endpoints) + .map(String::trim) + .map(this::newConnectorRow) + .toList(); + dsl.batchStore(connectorRecords).execute(); + } - Arrays.stream(knownConnectors).forEach(connectorId -> { - connectorId = connectorId.trim(); + @NotNull + private ConnectorRecord newConnectorRow(String endpoint) { + var connectorId = getEverythingBeforeThePath(endpoint); - var connectorRecord = ConnectorRecord.builder() - .id(connectorId) - .idsId(connectorId) - .endpoint(connectorId) - .createdAt(OffsetDateTime.now()) - .build(); + var connector = new ConnectorRecord(); + connector.setEndpoint(endpoint); + connector.setConnectorId(connectorId); + connector.setTitle("Unknown Connector"); + connector.setDescription("Awaiting initial crawling of given connector."); + connector.setIdsId(""); + connector.setCreatedAt(OffsetDateTime.now()); + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + return connector; + } - connectorStore.save(connectorRecord); - }); + /** + * Returns everything before the URLs path. + *

+ * Example: http://www.example.com/path/to/my/file.html -> http://www.example.com + * Example 2: http://www.example.com:9000/path/to/my/file.html -> http://www.example.com:9000 + * + * @param url url + * @return protocol, host, port + */ + private String getEverythingBeforeThePath(String url) { + var uri = URI.create(url); + String scheme = uri.getScheme(); // "http" + String authority = uri.getAuthority(); // "www.example.com" + int port = uri.getPort(); // -1 (no port specified) + String everythingBeforePath = scheme + "://" + authority; + if (port != -1) { + everythingBeforePath += ":" + port; + } + return everythingBeforePath; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 80938996e..ac1ebbcfd 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -14,8 +14,9 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorStore; +import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; @@ -32,28 +33,31 @@ @RequiredArgsConstructor public class ConnectorApiService { - private final ConnectorStore connectorStore; + private final DslContextFactory dslContextFactory; + private final ConnectorQueries connectorQueries; private final PaginationMetadataUtils paginationMetadataUtils; public ConnectorPageResult connectorPage(ConnectorPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - var result = new ConnectorPageResult(); + return dslContextFactory.transaction(dsl -> { + Objects.requireNonNull(query, "query must not be null"); - var connectors = connectorStore.findAll() - .map(ConnectorApiService::buildConnectorListEntry) - .sorted(Comparator.comparing(ConnectorListEntry::getTitle)) - .toList(); + var connectors = connectorQueries.findAll(dsl) + .map(ConnectorApiService::buildConnectorListEntry) + .sorted(Comparator.comparing(ConnectorListEntry::getTitle)) + .toList(); - result.setAvailableSortings(buildAvailableSortings()); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectors.size())); - result.setConnectors(connectors); - return result; + var result = new ConnectorPageResult(); + result.setAvailableSortings(buildAvailableSortings()); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectors.size())); + result.setConnectors(connectors); + return result; + }); } @NotNull private static ConnectorListEntry buildConnectorListEntry(ConnectorRecord it) { ConnectorListEntry dto = new ConnectorListEntry(); - dto.setId(it.getId()); + dto.setId(it.getEndpoint()); dto.setIdsId(it.getIdsId()); dto.setEndpoint(it.getEndpoint()); dto.setTitle(it.getTitle()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java index b874cecae..151b8160b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java @@ -14,20 +14,17 @@ package de.sovity.edc.ext.brokerserver.services.refreshing; -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorRecord; - /** * Fetch Connector Metadata. */ public class ConnectorSelfDescriptionFetcher { /** - * Fetches Connector metadata and returns an updated {@link ConnectorRecord} + * Fetches Connector metadata * - * @param connector existing / stubbed connector db row - * @return updated connector db row + * @param endpoint existing connector endpoint */ - public ConnectorRecord updateConnector(ConnectorRecord connector) { + public String fetchSelfDescription(String endpoint) { // TODO implement throw new IllegalArgumentException("Not yet implemented"); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java new file mode 100644 index 000000000..d4f8f6e89 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import de.sovity.edc.client.gen.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.List; + +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class ConnectorApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, List.of("https://example.com/ids/data"))); + } + + @Test + void testQueryConnectors() { + var result = edcClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); + assertThat(result.getConnectors()).hasSize(1); + assertThat(result.getConnectors().get(0).getEndpoint()).isEqualTo("https://example.com/ids/data"); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java new file mode 100644 index 000000000..86c2ec14e --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; + +public class TestUtils { + + private static final int DATA_PORT = getFreePort(); + public static final String MANAGEMENT_API_KEY = "123456"; + public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + "/api/v1/data"; + public static final String IDS_ENDPOINT = "http://localhost:" + DATA_PORT + "/api/v1/data/ids"; + + @NotNull + public static Map createConfiguration(TestDatabase testDatabase, List connectorEndpoints) { + Map config = new HashMap<>(); + config.put("web.http.port", String.valueOf(getFreePort())); + config.put("web.http.path", "/api"); + config.put("web.http.management.port", String.valueOf(DATA_PORT)); + config.put("web.http.management.path", "/api/v1/data"); + config.put("edc.api.auth.key", MANAGEMENT_API_KEY); + config.put("edc.ids.endpoint", IDS_ENDPOINT); + config.put(PostgresFlywayExtension.JDBC_URL, testDatabase.getJdbcUrl()); + config.put(PostgresFlywayExtension.JDBC_USER, testDatabase.getJdbcUser()); + config.put(PostgresFlywayExtension.JDBC_PASSWORD, testDatabase.getJdbcPassword()); + config.putAll(getCoreEdcJdbcConfig(testDatabase)); + config.put(BrokerServerExtension.KNOWN_CONNECTORS, String.join(",", connectorEndpoints)); + return config; + } + + private static Map getCoreEdcJdbcConfig(TestDatabase testDatabase) { + Map config = new HashMap<>(); + List.of("asset", + "contractdefinition", + "contractnegotiation", + "policy", + "transferprocess", + "dataplaneinstance" + ).forEach(it -> { + config.put("edc.datasource.%s.name".formatted(it), it); + config.put("edc.datasource.%s.url".formatted(it), testDatabase.getJdbcUrl()); + config.put("edc.datasource.%s.user".formatted(it), testDatabase.getJdbcUser()); + config.put("edc.datasource.%s.password".formatted(it), testDatabase.getJdbcPassword()); + }); + return config; + } + + public static EdcClient edcClient() { + return EdcClient.builder() + .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) + .managementApiKey(TestUtils.MANAGEMENT_API_KEY) + .build(); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java new file mode 100644 index 000000000..31f98c4e5 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; + +public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { + String getJdbcUrl(); + + String getJdbcUser(); + + String getJdbcPassword(); +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java new file mode 100644 index 000000000..a46fe2f0c --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TestDatabaseFactory { + + /** + * Returns a JUnit 5 Extension that either connects to a test database or launches a testcontainer. + * + * @return {@link TestDatabase} + */ + public static TestDatabase getTestDatabase() { + if (TestDatabaseViaEnv.isSkipTestcontainers()) { + return new TestDatabaseViaEnv(); + } + + return new TestDatabaseViaTestcontainers(); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java new file mode 100644 index 000000000..7d3f4be26 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import org.apache.commons.lang3.Validate; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.util.Objects; + +public class TestDatabaseViaEnv implements TestDatabase { + public static final String SKIP_TESTCONTAINERS = "SKIP_TESTCONTAINERS"; + public static final String TEST_POSTGRES_JDBC_URL = "TEST_POSTGRES_JDBC_URL"; + public static final String TEST_POSTGRES_JDBC_USER = "TEST_POSTGRES_JDBC_USER"; + public static final String TEST_POSTGRES_JDBC_PASSWORD = "TEST_POSTGRES_JDBC_PASSWORD"; + + @Override + public void afterAll(ExtensionContext context) throws Exception { + + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + + } + + public String getJdbcUrl() { + return getRequiredEnv(TEST_POSTGRES_JDBC_URL); + } + + public String getJdbcUser() { + return getRequiredEnv(TEST_POSTGRES_JDBC_USER); + } + + public String getJdbcPassword() { + return getRequiredEnv(TEST_POSTGRES_JDBC_PASSWORD); + } + + private static String getRequiredEnv(String name) { + String value = System.getenv(name); + Validate.notBlank(value, "Need env var %s since %s is true", name, SKIP_TESTCONTAINERS); + return value; + } + + public static boolean isSkipTestcontainers() { + return "true".equals(System.getenv(SKIP_TESTCONTAINERS)); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java new file mode 100644 index 000000000..b43fb3aa6 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.PostgreSQLContainer; + +public class TestDatabaseViaTestcontainers implements TestDatabase { + private PostgreSQLContainer container = new PostgreSQLContainer<>("postgres:15-alpine") + .withUsername("edc") + .withPassword("edc"); + + @Override + public void afterAll(ExtensionContext context) throws Exception { + container.stop(); + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + container.start(); + } + + public String getJdbcUrl() { + return container.getJdbcUrl(); + } + + public String getJdbcUser() { + return container.getUsername(); + } + + public String getJdbcPassword() { + return container.getPassword(); + } +} diff --git a/gradle.properties b/gradle.properties index 18c20a97d..cce85f653 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions -sovityEdcExtensionsVersion=3.1.1-SNAPSHOT +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc @@ -20,6 +20,7 @@ jsonVersion=20220924 restAssured=4.5.0 flywayVersion=9.0.1 postgresVersion=42.4.0 +testcontainersVersion=1.17.6 # Other Properties org.gradle.jvmargs=-Xmx1024m diff --git a/settings.gradle.kts b/settings.gradle.kts index c480d3dd4..d5f3d7136 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ rootProject.name = "edc-broker-server-extension" include(":extensions:broker-server") +include(":extensions:broker-server-postgres-flyway-jooq") include(":connector") From 75e189498f21b0855ba708d4130a4ab8b14e96d2 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Tue, 23 May 2023 12:53:28 +0200 Subject: [PATCH 014/295] feat: ConnectorSelfDescriptionFetcher (#38) * feat: ConnectorSelfDescriptionFetcher * feat: ConnectorSelfDescriptionFetcher * feat: ConnectorSelfDescriptionFetcher * feat: ConnectorSelfDescriptionFetcher * feat: ConnectorSelfDescriptionFetcher * feat: ConnectorSelfDescriptionFetcher * refactor: move RemoteMessageDispatcher to buildContext(...) * feat: add connector fetching to entry point, fix checkstyle errors, fix tests * chore: fix checkstyle --------- Co-authored-by: Richard Treier --- .../brokerserver/db/DslContextFactory.java | 2 +- .../brokerserver/BrokerServerExtension.java | 36 +++++++-- .../BrokerServerExtensionContext.java | 3 + .../BrokerServerExtensionContextBuilder.java | 50 ++++++++++++- .../dao/models/LogEventRecord.java | 5 ++ .../dao/stores/ConnectorQueries.java | 5 ++ .../services/BrokerEventLogger.java | 26 +++++-- .../services/BrokerServerInitializer.java | 41 ++++------ .../services/api/ConnectorApiService.java | 2 +- .../refreshing/ConnectorChangeTracker.java | 74 +++++++++++++++++++ .../refreshing/ConnectorSelfDescription.java | 18 +++++ .../ConnectorSelfDescriptionFetcher.java | 33 +++++++-- .../ConnectorUpdateFailureWriter.java | 46 ++++++++++++ .../ConnectorUpdateSuccessWriter.java | 67 +++++++++++++++++ .../services/refreshing/ConnectorUpdater.java | 37 +++++++++- .../refreshing/ContractOfferFetcher.java | 2 +- .../sender/DescriptionRequestSender.java | 65 ++++++++++++++++ .../sender/ExtendedMessageProtocol.java | 25 +++++++ ...tipartExtendedRemoteMessageDispatcher.java | 30 ++++++++ .../message/DescriptionRequestMessage.java | 32 ++++++++ .../edc/ext/brokerserver/utils/UrlUtils.java | 32 ++++++++ .../brokerserver/db/TestDatabaseViaEnv.java | 2 - 22 files changed, 575 insertions(+), 58 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java index 8dd33c0ce..5829ab09c 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java @@ -33,7 +33,7 @@ public DSLContext newDslContext() { * @param return type * @return new {@link DSLContext} + opened transaction */ - public R transaction(Function function) { + public R transactionResult(Function function) { return newDslContext().transactionResult(transaction -> function.apply(transaction.dsl())); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index fe4e00b43..1c1937c2d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -15,11 +15,14 @@ package de.sovity.edc.ext.brokerserver; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Setting; -import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.web.spi.WebService; public class BrokerServerExtension implements ServiceExtension { @@ -29,17 +32,26 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String KNOWN_CONNECTORS = "edc.brokerserver.known.connectors"; - @Inject - private AssetIndex assetIndex; // ensures db is initialized before - @Inject private ManagementApiConfiguration managementApiConfiguration; @Inject private WebService webService; + @Inject + private EdcHttpClient httpClient; + + @Inject + private DynamicAttributeTokenService dynamicAttributeTokenService; + + @Inject + private TypeManager typeManager; + + @Inject + private RemoteMessageDispatcherRegistry dispatcherRegistry; + /** - * Manual Dependency Injection + * Manual Dependency Injection Result */ private BrokerServerExtensionContext services; @@ -50,9 +62,19 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - services = BrokerServerExtensionContextBuilder.buildContext(context.getConfig()); - String managementApiGroup = managementApiConfiguration.getContextAlias(); + services = BrokerServerExtensionContextBuilder.buildContext( + context.getConfig(), + context.getMonitor(), + httpClient, + dynamicAttributeTokenService, + typeManager, + dispatcherRegistry + ); + + var managementApiGroup = managementApiConfiguration.getContextAlias(); webService.registerResource(managementApiGroup, services.brokerServerResource()); + + dispatcherRegistry.register(services.remoteMessageDispatcher()); } @Override diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index 992718b86..01571c84f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -16,15 +16,18 @@ import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; +import org.eclipse.edc.spi.message.RemoteMessageDispatcher; /** * Manual Dependency Injection result * + * @param remoteMessageDispatcher IDS Message Client * @param brokerServerResource REST Resource with API Endpoint implementations * @param brokerServerInitializer Startup Logic */ public record BrokerServerExtensionContext( + RemoteMessageDispatcher remoteMessageDispatcher, BrokerServerResource brokerServerResource, BrokerServerInitializer brokerServerInitializer ) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index a049cffef..b2f43a10b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -16,15 +16,30 @@ import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; import de.sovity.edc.ext.brokerserver.dao.stores.ContractOfferStore; +import de.sovity.edc.ext.brokerserver.dao.stores.LogEventStore; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorSelfDescriptionFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; +import de.sovity.edc.ext.brokerserver.services.refreshing.ContractOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.sender.DescriptionRequestSender; +import de.sovity.edc.ext.brokerserver.services.refreshing.sender.IdsMultipartExtendedRemoteMessageDispatcher; import lombok.NoArgsConstructor; +import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.IdsMultipartSender; +import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.types.TypeManager; /** @@ -37,16 +52,45 @@ */ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class BrokerServerExtensionContextBuilder { - public static BrokerServerExtensionContext buildContext(Config config) { + + public static BrokerServerExtensionContext buildContext( + Config config, + Monitor monitor, + EdcHttpClient httpClient, + DynamicAttributeTokenService dynamicAttributeTokenService, + TypeManager typeManager, + RemoteMessageDispatcherRegistry dispatcherRegistry + ) { // Dao var dataSourceFactory = new DataSourceFactory(config); var dataSource = dataSourceFactory.newDataSource(); var dslContextFactory = new DslContextFactory(dataSource); var connectorQueries = new ConnectorQueries(); var contractOfferStore = new ContractOfferStore(); + var logEventStore = new LogEventStore(); + + // IDS Message Client + var objectMapper = typeManager.getMapper(); + var idsMultipartSender = new IdsMultipartSender(monitor, httpClient, dynamicAttributeTokenService, objectMapper); + var remoteMessageDispatcher = new IdsMultipartExtendedRemoteMessageDispatcher(idsMultipartSender); + var descriptionRequestSender = new DescriptionRequestSender(); // Services - var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config); + var connectorSelfDescriptionFetcher = new ConnectorSelfDescriptionFetcher(dispatcherRegistry); + var brokerEventLogger = new BrokerEventLogger(logEventStore); + var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger); + var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger); + var contractOfferFetcher = new ContractOfferFetcher(); + var connectorUpdater = new ConnectorUpdater( + connectorSelfDescriptionFetcher, + connectorUpdateSuccessWriter, + connectorUpdateFailureWriter, + contractOfferFetcher, + connectorQueries, + dslContextFactory, + monitor + ); + var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config, connectorUpdater); // UI Capabilities var paginationMetadataUtils = new PaginationMetadataUtils(); @@ -60,6 +104,6 @@ public static BrokerServerExtensionContext buildContext(Config config) { paginationMetadataUtils ); var brokerServerResource = new BrokerServerResourceImpl(connectorApiService, catalogApiService); - return new BrokerServerExtensionContext(brokerServerResource, brokerServerInitializer); + return new BrokerServerExtensionContext(remoteMessageDispatcher, brokerServerResource, brokerServerInitializer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java index ea9cf2789..54012857f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java @@ -72,6 +72,11 @@ public class LogEventRecord { */ String userMessage; + /** + * Error Stack Trace + */ + String error; + /** * Execution time in milliseconds, if recorded / applicable */ diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java index 8480712b9..9e2d83a27 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java @@ -25,4 +25,9 @@ public class ConnectorQueries { public Stream findAll(DSLContext dslContext) { return dslContext.selectFrom(Tables.CONNECTOR).stream(); } + + public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { + var c = Tables.CONNECTOR; + return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java index 3cb33f44a..7b67028f1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java @@ -18,8 +18,9 @@ import de.sovity.edc.ext.brokerserver.dao.models.LogEventStatus; import de.sovity.edc.ext.brokerserver.dao.models.LogEventType; import de.sovity.edc.ext.brokerserver.dao.stores.LogEventStore; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorChangeTracker; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.exception.ExceptionUtils; import java.time.OffsetDateTime; @@ -30,15 +31,30 @@ public class BrokerEventLogger { private final LogEventStore logEventStore; - public void logConnectorUpdate(ConnectorRecord connector, LogEventStatus outcome) { - var logEntry = LogEventRecord.builder() - .connectorEndpoint(connector.getEndpoint()) + public void logConnectorUpdateSuccess(String connectorEndpoint, ConnectorChangeTracker changes) { + var status = changes.isEmpty() ? LogEventStatus.UNCHANGED : LogEventStatus.UPDATED; + var logEntry = connectorUpdateLogEntry(connectorEndpoint, status).toBuilder() + .userMessage(changes.getLogMessage()) + .build(); + this.logEventStore.save(logEntry); + } + + public void logConnectorUpdateFailure(String connectorEndpoint, String message, Throwable exceptionOrNull) { + var logEntry = connectorUpdateLogEntry(connectorEndpoint, LogEventStatus.ERROR).toBuilder() + .userMessage(message) + .error(exceptionOrNull == null ? null : ExceptionUtils.getStackTrace(exceptionOrNull)) + .build(); + this.logEventStore.save(logEntry); + } + + private LogEventRecord connectorUpdateLogEntry(String connectorEndpoint, LogEventStatus outcome) { + return LogEventRecord.builder() + .connectorEndpoint(connectorEndpoint) .userMessage(getConnectorUpdatedMessage(outcome)) .type(LogEventType.CONNECTOR_UPDATED) .createdAt(OffsetDateTime.now()) .status(outcome) .build(); - this.logEventStore.save(logEntry); } private static String getConnectorUpdatedMessage(LogEventStatus outcome) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java index c9fbc06ad..349d46753 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java @@ -18,27 +18,33 @@ import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; +import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.system.configuration.Config; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import java.net.URI; import java.time.OffsetDateTime; -import java.util.Arrays; +import java.util.List; @RequiredArgsConstructor public class BrokerServerInitializer { private final DslContextFactory dslContextFactory; private final Config config; + private final ConnectorUpdater connectorUpdater; + public void onStartup() { - dslContextFactory.transaction(this::initializeConnectorList); + List connectorEndpoints = getPreconfiguredConnectorEndpoints(); + dslContextFactory.transaction(dsl -> initializeConnectorList(dsl, connectorEndpoints)); + + // TODO fill queue rather than execute in loop + connectorEndpoints.forEach(connectorUpdater::updateConnector); } - private void initializeConnectorList(DSLContext dsl) { - var endpoints = config.getString(BrokerServerExtension.KNOWN_CONNECTORS).split(","); - var connectorRecords = Arrays.stream(endpoints) + private void initializeConnectorList(DSLContext dsl, List connectorEndpoints) { + var connectorRecords = connectorEndpoints.stream() .map(String::trim) .map(this::newConnectorRow) .toList(); @@ -47,7 +53,7 @@ private void initializeConnectorList(DSLContext dsl) { @NotNull private ConnectorRecord newConnectorRow(String endpoint) { - var connectorId = getEverythingBeforeThePath(endpoint); + var connectorId = UrlUtils.getEverythingBeforeThePath(endpoint); var connector = new ConnectorRecord(); connector.setEndpoint(endpoint); @@ -60,24 +66,7 @@ private ConnectorRecord newConnectorRow(String endpoint) { return connector; } - /** - * Returns everything before the URLs path. - *

- * Example: http://www.example.com/path/to/my/file.html -> http://www.example.com - * Example 2: http://www.example.com:9000/path/to/my/file.html -> http://www.example.com:9000 - * - * @param url url - * @return protocol, host, port - */ - private String getEverythingBeforeThePath(String url) { - var uri = URI.create(url); - String scheme = uri.getScheme(); // "http" - String authority = uri.getAuthority(); // "www.example.com" - int port = uri.getPort(); // -1 (no port specified) - String everythingBeforePath = scheme + "://" + authority; - if (port != -1) { - everythingBeforePath += ":" + port; - } - return everythingBeforePath; + private List getPreconfiguredConnectorEndpoints() { + return List.of(config.getString(BrokerServerExtension.KNOWN_CONNECTORS).split(",")); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index ac1ebbcfd..15780198c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -38,7 +38,7 @@ public class ConnectorApiService { private final PaginationMetadataUtils paginationMetadataUtils; public ConnectorPageResult connectorPage(ConnectorPageQuery query) { - return dslContextFactory.transaction(dsl -> { + return dslContextFactory.transactionResult(dsl -> { Objects.requireNonNull(query, "query must not be null"); var connectors = connectorQueries.findAll(dsl) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java new file mode 100644 index 000000000..2ae70f293 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.stream.Collectors.joining; + +/** + * Utility for collecting the information required to build log messages about what was updated. + */ +@Getter +public class ConnectorChangeTracker { + private final List selfDescriptionChanges = new ArrayList<>(); + + @Setter + private int numOffersAdded = 0; + + @Setter + private int numOffersDeleted = 0; + + @Setter + private int numOffersUpdated = 0; + + + public void addSelfDescriptionChange(String name) { + selfDescriptionChanges.add(name); + } + + public boolean isEmpty() { + return selfDescriptionChanges.isEmpty(); + } + + public String getLogMessage() { + if (isEmpty()) { + return "Connector is up to date."; + } + + String msg = "Connector Updated."; + if (!selfDescriptionChanges.isEmpty()) { + msg += " Self-description changed: %s.".formatted(selfDescriptionChanges.stream().sorted().collect(joining())); + } + if (numOffersAdded > 0 || numOffersDeleted > 0 || numOffersUpdated > 0) { + List offersMsgs = new ArrayList<>(); + if (numOffersAdded > 0) { + offersMsgs.add("%d added".formatted(numOffersAdded)); + } + if (numOffersUpdated > 0) { + offersMsgs.add("%d updated".formatted(numOffersUpdated)); + } + if (numOffersDeleted > 0) { + offersMsgs.add("%d deleted".formatted(numOffersDeleted)); + } + msg += " Data Offers changed: %s.".formatted(String.join(", ", offersMsgs)); + } + return msg; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java new file mode 100644 index 000000000..a0c5ef0c9 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +public record ConnectorSelfDescription(String idsId, String title, String description) { +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java index 151b8160b..64f038e3a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java @@ -14,18 +14,35 @@ package de.sovity.edc.ext.brokerserver.services.refreshing; +import de.sovity.edc.ext.brokerserver.services.refreshing.sender.message.DescriptionRequestMessage; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; + +import java.net.MalformedURLException; +import java.net.URL; + /** * Fetch Connector Metadata. */ +@RequiredArgsConstructor public class ConnectorSelfDescriptionFetcher { + private static final String CONTEXT_SD_REQUEST = "SelfDescriptionRequest"; + + private final RemoteMessageDispatcherRegistry dispatcherRegistry; + + public ConnectorSelfDescription fetch(String connectorEndpoint) { + try { + var connectorEndpointUrl = new URL(connectorEndpoint); + var descriptionRequestMessage = new DescriptionRequestMessage(connectorEndpointUrl); + var descriptionResponse = dispatcherRegistry.send(String.class, descriptionRequestMessage, () -> CONTEXT_SD_REQUEST).get(); - /** - * Fetches Connector metadata - * - * @param endpoint existing connector endpoint - */ - public String fetchSelfDescription(String endpoint) { - // TODO implement - throw new IllegalArgumentException("Not yet implemented"); + // TODO parse self-description + return new ConnectorSelfDescription("TODO", "TODO", descriptionResponse); + } catch (MalformedURLException e) { + throw new EdcException("Invalid connector-endpoint URL", e); + } catch (Exception e) { + throw new EdcException("Failed to fetch connector self-description", e); + } } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java new file mode 100644 index 000000000..533639f5d --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.EdcException; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; + +@RequiredArgsConstructor +public class ConnectorUpdateFailureWriter { + private final BrokerEventLogger brokerEventLogger; + + public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Throwable e) { + // Update Connector + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + connector.setLastUpdate(OffsetDateTime.now()); + connector.setOfflineSince(OffsetDateTime.now()); + connector.update(); + + // Log Event + Throwable stackTrace = shouldLogStacktrace(e) ? e : null; + String message = "Failed updating connector: %s".formatted(e.getMessage()); + brokerEventLogger.logConnectorUpdateFailure(connector.getEndpoint(), message, stackTrace); + } + + private boolean shouldLogStacktrace(Throwable e) { + return !(e instanceof EdcException); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java new file mode 100644 index 000000000..55045be2b --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +public class ConnectorUpdateSuccessWriter { + private final BrokerEventLogger brokerEventLogger; + + public void handleConnectorOnline( + DSLContext dsl, + ConnectorRecord connector, + ConnectorSelfDescription selfDescription, + List contractOffers + ) { + // Track changes for final log message + ConnectorChangeTracker changes = new ConnectorChangeTracker(); + updateConnector(connector, selfDescription, changes); + + // Update contract offers (if changed) + // TODO + + // Log Event + brokerEventLogger.logConnectorUpdateSuccess(connector.getEndpoint(), changes); + } + + private static void updateConnector(ConnectorRecord connector, ConnectorSelfDescription selfDescription, ConnectorChangeTracker changes) { + if (!Objects.equals(selfDescription.idsId(), connector.getIdsId())) { + changes.addSelfDescriptionChange("IDS Connector ID"); + connector.setIdsId(selfDescription.idsId()); + } + if (!Objects.equals(selfDescription.title(), connector.getTitle())) { + changes.addSelfDescriptionChange("Title"); + connector.setIdsId(selfDescription.title()); + } + if (!Objects.equals(selfDescription.description(), connector.getDescription())) { + changes.addSelfDescriptionChange("Description"); + connector.setIdsId(selfDescription.description()); + } + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setLastUpdate(OffsetDateTime.now()); + connector.setOfflineSince(null); + connector.update(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index 90977e6a2..b485dd37e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -14,8 +14,14 @@ package de.sovity.edc.ext.brokerserver.services.refreshing; -import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.util.List; /** * Updates a single connector. @@ -23,8 +29,12 @@ @RequiredArgsConstructor public class ConnectorUpdater { private final ConnectorSelfDescriptionFetcher connectorSelfDescriptionFetcher; + private final ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter; + private final ConnectorUpdateFailureWriter connectorUpdateFailureWriter; private final ContractOfferFetcher contractOfferFetcher; - private final BrokerEventLogger brokerEventLogger; + private final ConnectorQueries connectorQueries; + private final DslContextFactory dslContextFactory; + private final Monitor monitor; /** * Updates single connector. @@ -32,7 +42,26 @@ public class ConnectorUpdater { * @param connectorEndpoint connector endpoint */ public void updateConnector(String connectorEndpoint) { - // TODO implement - throw new IllegalArgumentException("Not yet implemented"); + try { + ConnectorSelfDescription selfDescription = connectorSelfDescriptionFetcher.fetch(connectorEndpoint); + List contractOffers = contractOfferFetcher.fetch(connectorEndpoint); + + // Update connector in a single transaction + dslContextFactory.transaction(dsl -> { + ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); + connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, selfDescription, contractOffers); + }); + } catch (Exception e) { + try { + // Update connector in a single transaction + dslContextFactory.transaction(dsl -> { + ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); + connectorUpdateFailureWriter.handleConnectorOffline(dsl, connectorRecord, e); + }); + } catch (Exception e1) { + e1.addSuppressed(e); + monitor.severe("Failed updating connector as failed.", e1); + } + } } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java index 87c1dc4a8..fd4eccc95 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java @@ -26,7 +26,7 @@ public class ContractOfferFetcher { * @param connectorEndpoint connector endpoint * @return updated connector db row */ - public List fetchContractOffers(String connectorEndpoint) { + public List fetch(String connectorEndpoint) { // TODO implement throw new IllegalArgumentException("Not yet implemented"); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java new file mode 100644 index 000000000..1820cd9c6 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.sender; + +import de.fraunhofer.iais.eis.DescriptionRequestMessageBuilder; +import de.fraunhofer.iais.eis.DescriptionResponseMessage; +import de.fraunhofer.iais.eis.DynamicAttributeToken; +import de.fraunhofer.iais.eis.Message; +import de.sovity.edc.ext.brokerserver.services.refreshing.sender.message.DescriptionRequestMessage; +import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.MultipartSenderDelegate; +import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.response.IdsMultipartParts; +import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.response.MultipartResponse; +import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; +import org.eclipse.edc.protocol.ids.util.CalendarUtil; + +import java.net.URI; +import java.util.List; + +import static org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.util.ResponseUtil.parseMultipartStringResponse; +import static org.eclipse.edc.protocol.ids.jsonld.JsonLd.getObjectMapper; + +public class DescriptionRequestSender implements MultipartSenderDelegate { + @Override + public Message buildMessageHeader(DescriptionRequestMessage request, DynamicAttributeToken token) throws Exception { + return new DescriptionRequestMessageBuilder() + ._modelVersion_(IdsConstants.INFORMATION_MODEL_VERSION) + ._issued_(CalendarUtil.gregorianNow()) + ._securityToken_(token) + ._issuerConnector_(new URI(request.getConnectorAddress())) + ._senderAgent_(new URI(request.getConnectorAddress())) + .build(); + } + + @Override + public String buildMessagePayload(DescriptionRequestMessage request) throws Exception { + return null; + } + + @Override + public MultipartResponse getResponseContent(IdsMultipartParts parts) throws Exception { + return parseMultipartStringResponse(parts, getObjectMapper()); + } + + @Override + public List> getAllowedResponseTypes() { + return List.of(DescriptionResponseMessage.class); + } + + @Override + public Class getMessageType() { + return DescriptionRequestMessage.class; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java new file mode 100644 index 000000000..4c1262ba0 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.sender; + +import org.eclipse.edc.protocol.ids.spi.types.MessageProtocol; + +public final class ExtendedMessageProtocol { + private static final String EXTENDED_SUFFIX = "-extended"; + public static final String IDS_EXTENDED_PROTOCOL = String.format("%s%s", MessageProtocol.IDS_MULTIPART, EXTENDED_SUFFIX); + + private ExtendedMessageProtocol() { + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java new file mode 100644 index 000000000..1bb844166 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.sender; + +import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.IdsMultipartRemoteMessageDispatcher; +import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.IdsMultipartSender; + +public class IdsMultipartExtendedRemoteMessageDispatcher extends IdsMultipartRemoteMessageDispatcher { + + public IdsMultipartExtendedRemoteMessageDispatcher(IdsMultipartSender idsMultipartSender) { + super(idsMultipartSender); + } + + @Override + public String protocol() { + return ExtendedMessageProtocol.IDS_EXTENDED_PROTOCOL; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java new file mode 100644 index 000000000..5c3377781 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.sender.message; + +import de.sovity.edc.ext.brokerserver.services.refreshing.sender.ExtendedMessageProtocol; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.net.URL; + +public record DescriptionRequestMessage(URL connectorEndpoint) implements RemoteMessage { + @Override + public String getProtocol() { + return ExtendedMessageProtocol.IDS_EXTENDED_PROTOCOL; + } + + @Override + public String getConnectorAddress() { + return connectorEndpoint.toString(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java new file mode 100644 index 000000000..5aee1c1bb --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java @@ -0,0 +1,32 @@ +package de.sovity.edc.ext.brokerserver.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.net.URI; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UrlUtils { + + + /** + * Returns everything before the URLs path. + *

+ * Example: http://www.example.com/path/to/my/file.html -> http://www.example.com + * Example 2: http://www.example.com:9000/path/to/my/file.html -> http://www.example.com:9000 + * + * @param url url + * @return protocol, host, port + */ + public static String getEverythingBeforeThePath(String url) { + var uri = URI.create(url); + String scheme = uri.getScheme(); // "http" + String authority = uri.getAuthority(); // "www.example.com" + int port = uri.getPort(); // -1 (no port specified) + String everythingBeforePath = scheme + "://" + authority; + if (port != -1) { + everythingBeforePath += ":" + port; + } + return everythingBeforePath; + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java index 7d3f4be26..ad2a5c5f9 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java @@ -17,8 +17,6 @@ import org.apache.commons.lang3.Validate; import org.junit.jupiter.api.extension.ExtensionContext; -import java.util.Objects; - public class TestDatabaseViaEnv implements TestDatabase { public static final String SKIP_TESTCONTAINERS = "SKIP_TESTCONTAINERS"; public static final String TEST_POSTGRES_JDBC_URL = "TEST_POSTGRES_JDBC_URL"; From 0b7434ebd0b03c622bc0ded3f35f3d848830a601 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 23 May 2023 14:21:30 +0200 Subject: [PATCH 015/295] chore: issues are no longer added to the sovity gh project --- .github/workflows/add_issue_to_project.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index d2cbf15db..60d0da4ba 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -10,10 +10,6 @@ jobs: name: add_issue_to_project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/sovity/projects/9 - github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} - uses: actions/add-to-project@v0.5.0 with: project-url: https://github.com/orgs/sovity/projects/21 From b9f93737e455a7e1ef81d2c7d4ab89e07026c8e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 07:10:46 +0200 Subject: [PATCH 016/295] chore(deps): bump org.postgresql:postgresql from 42.5.0 to 42.6.0 (#66) Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.5.0 to 42.6.0. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.5.0...REL42.6.0) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../broker-server-postgres-flyway-jooq/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 50070881a..6809f850f 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -34,8 +34,8 @@ plugins { dependencies { api("org.jooq:jooq:3.16.4") - jooqGenerator("org.postgresql:postgresql:42.5.0") - flywayMigration("org.postgresql:postgresql:42.5.0") + jooqGenerator("org.postgresql:postgresql:42.6.0") + flywayMigration("org.postgresql:postgresql:42.6.0") annotationProcessor("org.projectlombok:lombok:1.18.26") compileOnly("org.projectlombok:lombok:1.18.26") From 0d8bad5fe4ec510fd3e267975dda4d3ef15e302a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 07:11:20 +0200 Subject: [PATCH 017/295] chore(deps): bump org.testcontainers:postgresql from 1.17.6 to 1.18.1 (#64) Bumps [org.testcontainers:postgresql](https://github.com/testcontainers/testcontainers-java) from 1.17.6 to 1.18.1. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.17.6...1.18.1) --- updated-dependencies: - dependency-name: org.testcontainers:postgresql dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 6809f850f..06a368dbe 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -21,7 +21,7 @@ val postgresVersion: String by project buildscript { dependencies { - classpath("org.testcontainers:postgresql:1.17.6") + classpath("org.testcontainers:postgresql:1.18.1") } } From 5c82dc537b49f6613aaa5ead38afa2c69da6cfbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 07:11:43 +0200 Subject: [PATCH 018/295] chore(deps): bump org.flywaydb.flyway from 9.3.0 to 9.19.0 (#63) Bumps org.flywaydb.flyway from 9.3.0 to 9.19.0. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 06a368dbe..cc4cc6e6e 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -26,7 +26,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.3.0" + id("org.flywaydb.flyway") version "9.19.0" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From ae1d059fff0a3946a1a40c7377d095c8b31d334b Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 24 May 2023 08:26:40 +0200 Subject: [PATCH 019/295] feat: connectorQueue (#32) * feat: ConnectorQueue * feat: ConnectorQueue --- .../BrokerServerExtensionContextBuilder.java | 7 +++- .../services/BrokerServerInitializer.java | 9 +++-- .../services/ConnectorQueueEntry.java | 33 +++++++++++++++++++ .../ext/brokerserver/services/queue/.gitkeep | 0 .../services/queue/ConnectorQueue.java | 30 +++++++++++++++++ .../queue/ConnectorRefreshPriority.java | 20 +++++++++++ 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/.gitkeep create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index b2f43a10b..3c5ee75b9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -24,6 +24,7 @@ import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorSelfDescriptionFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; @@ -90,7 +91,11 @@ public static BrokerServerExtensionContext buildContext( dslContextFactory, monitor ); - var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config, connectorUpdater); + + // Queue + var connectorQueue = new ConnectorQueue(); + + var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config, connectorQueue); // UI Capabilities var paginationMetadataUtils = new PaginationMetadataUtils(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java index 349d46753..f3d7a46c0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java @@ -18,7 +18,8 @@ import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.system.configuration.Config; @@ -32,15 +33,13 @@ public class BrokerServerInitializer { private final DslContextFactory dslContextFactory; private final Config config; - - private final ConnectorUpdater connectorUpdater; + private final ConnectorQueue connectorQueue; public void onStartup() { List connectorEndpoints = getPreconfiguredConnectorEndpoints(); dslContextFactory.transaction(dsl -> initializeConnectorList(dsl, connectorEndpoints)); - // TODO fill queue rather than execute in loop - connectorEndpoints.forEach(connectorUpdater::updateConnector); + connectorEndpoints.forEach(it -> connectorQueue.add(new ConnectorQueueEntry(it, OffsetDateTime.now(), ConnectorRefreshPriority.ADDED_ON_STARTUP))); } private void initializeConnectorList(DSLContext dsl, List connectorEndpoints) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java new file mode 100644 index 000000000..bfcbd8cf8 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import org.jetbrains.annotations.NotNull; + +import java.time.OffsetDateTime; +import java.util.Comparator; + +public record ConnectorQueueEntry(String endpoint, + OffsetDateTime lastUpdate, + int priority) implements Comparable { + private static final Comparator COMPARATOR = Comparator + .comparing(ConnectorQueueEntry::priority) + .thenComparing(ConnectorQueueEntry::lastUpdate); + + @Override + public int compareTo(@NotNull ConnectorQueueEntry connectorQueueEntry) { + return COMPARATOR.compare(this, connectorQueueEntry); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/.gitkeep b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java new file mode 100644 index 000000000..adbb40fd0 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.brokerserver.services.queue; + +import de.sovity.edc.ext.brokerserver.services.ConnectorQueueEntry; + +import java.util.concurrent.PriorityBlockingQueue; + +public class ConnectorQueue { + private final PriorityBlockingQueue connectorQueueEntries = new PriorityBlockingQueue<>(); + + public void add(ConnectorQueueEntry entry) { + connectorQueueEntries.add(entry); + } + + public ConnectorQueueEntry poll() { + return connectorQueueEntries.poll(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java new file mode 100644 index 000000000..b118cca0c --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.brokerserver.services.queue; + +public class ConnectorRefreshPriority { + public static final int ADMIN_REQUESTED = 1; + public static final int ADDED_ON_STARTUP = 10; + public static final int SCHEDULED_REFRESH = 100; +} From 4c0001cdcc8cf5f2ef0ded6acdff204da7e8ba37 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 24 May 2023 13:50:05 +0200 Subject: [PATCH 020/295] ConnectorPageApiService + BrokerEventLog (#68) * feat: table broker_event_log * feat: ConnectorApiService --- .../V2__Broker_Server_Initial_DB_Model.sql | 82 ++++++++++++++++-- .../BrokerServerExtensionContextBuilder.java | 11 +-- .../dao/models/ConnectorPageDbRow.java | 39 +++++++++ .../dao/models/ContractOfferRecord.java | 37 -------- .../dao/models/LogEventRecord.java | 84 ------------------- .../dao/models/LogEventStatus.java | 35 -------- .../brokerserver/dao/models/LogEventType.java | 51 ----------- .../dao/queries/ConnectorQueries.java | 68 +++++++++++++++ .../dao/queries/utils/LikeUtils.java | 57 +++++++++++++ .../dao/queries/utils/SearchUtils.java | 51 +++++++++++ .../dao/stores/ConnectorQueries.java | 33 -------- .../dao/stores/ContractOfferStore.java | 39 --------- .../dao/stores/LogEventStore.java | 40 --------- .../services/BrokerEventLogger.java | 68 --------------- .../services/api/CatalogApiService.java | 2 - .../services/api/ConnectorApiService.java | 27 +++--- .../logging/BrokerEventErrorMessage.java | 43 ++++++++++ .../services/logging/BrokerEventLogger.java | 62 ++++++++++++++ .../ConnectorChangeTracker.java | 5 +- .../services/queue/ConnectorQueue.java | 1 + .../queue/ConnectorRefreshPriority.java | 1 + .../ConnectorUpdateFailureWriter.java | 17 ++-- .../ConnectorUpdateSuccessWriter.java | 5 +- .../services/refreshing/ConnectorUpdater.java | 2 +- .../ext/brokerserver/utils/StringUtils2.java | 58 +++++++++++++ .../edc/ext/brokerserver/utils/UrlUtils.java | 14 ++++ .../ext/brokerserver/ConnectorApiTest.java | 4 +- .../dao/queries/utils/LikeUtilsTest.java | 26 ++++++ .../brokerserver/utils/StringUtils2Test.java | 62 ++++++++++++++ 29 files changed, 596 insertions(+), 428 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/{refreshing => logging}/ConnectorChangeTracker.java (95%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index 9d3318b16..be578ead0 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -2,10 +2,10 @@ create type connector_online_status as enum ('ONLINE', 'OFFLINE'); create table connector ( - endpoint varchar(512) not null, - connector_id varchar(512) not null, - ids_id varchar(512) not null, - title varchar(512), + endpoint text not null, + connector_id text not null, + ids_id text not null, + title text, description text, last_update timestamp with time zone, offline_since timestamp with time zone, @@ -13,4 +13,76 @@ create table connector online_status connector_online_status not null, PRIMARY KEY (endpoint) -); \ No newline at end of file +); + +create table data_offer +( + connector_endpoint text not null, + asset_id text not null, + asset_properties jsonb not null, + created_at timestamp with time zone not null, + updated_at timestamp with time zone, + + PRIMARY KEY (connector_endpoint, asset_id), + FOREIGN KEY (connector_endpoint) REFERENCES connector (endpoint) +); + +create table data_offer_contract_offer +( + contract_offer_id text not null, + connector_endpoint text not null, + asset_id text not null, + policy jsonb not null, + created_at timestamp with time zone not null, + updated_at timestamp with time zone, + + PRIMARY KEY (contract_offer_id), + FOREIGN KEY (connector_endpoint, asset_id) REFERENCES data_offer (connector_endpoint, asset_id), + FOREIGN KEY (connector_endpoint) REFERENCES connector (endpoint) +); + +create type broker_event_type as enum ( + --Connector was successfully updated, and changes were incorporated + 'CONNECTOR_UPDATED', + + --Connector went online + 'CONNECTOR_STATUS_CHANGE_ONLINE', + + --Connector went offline + 'CONNECTOR_STATUS_CHANGE_OFFLINE', + + --Connector was "force deleted" + 'CONNECTOR_STATUS_CHANGE_FORCE_DELETED', + + --Contract Offer was updated + 'CONTRACT_OFFER_UPDATED', + + --Contract Offer was clicked + 'CONTRACT_OFFER_CLICK' +); + +create type broker_event_status as enum ( + -- Default + 'OK', + + -- Failures + 'ERROR', + + -- E.g. refreshes, that resulted in no changes + 'UNCHANGED' +); + +create table broker_event_log +( + id serial primary key, + created_at timestamp with time zone not null, + user_message text not null, + event broker_event_type not null, + event_status broker_event_status not null, + connector_endpoint text, + asset_id text, + error_stack text, + duration_in_ms bigint +); + +create index speedup on broker_event_log (connector_endpoint, asset_id, event_status); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 3c5ee75b9..0642cd7fe 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -14,16 +14,14 @@ package de.sovity.edc.ext.brokerserver; -import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.dao.stores.ContractOfferStore; -import de.sovity.edc.ext.brokerserver.dao.stores.LogEventStore; +import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorSelfDescriptionFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; @@ -67,8 +65,6 @@ public static BrokerServerExtensionContext buildContext( var dataSource = dataSourceFactory.newDataSource(); var dslContextFactory = new DslContextFactory(dataSource); var connectorQueries = new ConnectorQueries(); - var contractOfferStore = new ContractOfferStore(); - var logEventStore = new LogEventStore(); // IDS Message Client var objectMapper = typeManager.getMapper(); @@ -78,7 +74,7 @@ public static BrokerServerExtensionContext buildContext( // Services var connectorSelfDescriptionFetcher = new ConnectorSelfDescriptionFetcher(dispatcherRegistry); - var brokerEventLogger = new BrokerEventLogger(logEventStore); + var brokerEventLogger = new BrokerEventLogger(); var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger); var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger); var contractOfferFetcher = new ContractOfferFetcher(); @@ -100,7 +96,6 @@ public static BrokerServerExtensionContext buildContext( // UI Capabilities var paginationMetadataUtils = new PaginationMetadataUtils(); var catalogApiService = new CatalogApiService( - contractOfferStore, paginationMetadataUtils ); var connectorApiService = new ConnectorApiService( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java new file mode 100644 index 000000000..a7664ca13 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ConnectorPageDbRow { + String endpoint; + String connectorId; + String idsId; + String title; + String description; + OffsetDateTime lastUpdate; + OffsetDateTime offlineSince; + OffsetDateTime createdAt; + ConnectorOnlineStatus onlineStatus; + Integer numDataOffers; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java deleted file mode 100644 index 6f34a2677..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ContractOfferRecord.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.models; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.experimental.FieldDefaults; - -/** - * Contract Offer Database Row that can be inserted or updated. - *

- * Represents a data offer in the data space. - */ -@Getter -@ToString -@Builder(toBuilder = true) -@EqualsAndHashCode(of = "id") -@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) -@FieldDefaults(makeFinal = true, level = lombok.AccessLevel.PRIVATE) -public class ContractOfferRecord { - String id; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java deleted file mode 100644 index 54012857f..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventRecord.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.models; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; - -/** - * Log Event Database Row that can be inserted or updated. - *

- * Many kinds of events or tasks might log into this table. - * Logging of execution times is also supported. - */ -@Getter -@ToString -@Builder(toBuilder = true) -@EqualsAndHashCode(of = "id") -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) -public class LogEventRecord { - /** - * Row ID - */ - String id; - - /** - * Log Message Date - */ - OffsetDateTime createdAt; - - /** - * Log Entry Type - */ - LogEventType type; - - /** - * Log Entry Type - */ - LogEventStatus status; - - /** - * Connector reference, if applicable - */ - String connectorEndpoint; - - /** - * Contract Offer reference, if applicable - */ - String contractOfferId; - - /** - * Message to be shown in UI, if applicable - */ - String userMessage; - - /** - * Error Stack Trace - */ - String error; - - /** - * Execution time in milliseconds, if recorded / applicable - */ - Long executionTimeInMs; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java deleted file mode 100644 index b99befa58..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventStatus.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.models; - -/** - * This enum allows us to differentiate errors and changeful events. - */ -public enum LogEventStatus { - /** - * Means that something failed. - */ - ERROR, - - /** - * Means that these log messages can basically be skipped. - */ - UNCHANGED, - - /** - * A log message that might interest the user (compared to log messages kept to calculate execution times) - */ - UPDATED; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java deleted file mode 100644 index 987173fc9..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/LogEventType.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.models; - -/** - * The table {@link LogEventRecord} table contains many types of events or task outcomes. - * This enum is used to distinguish between them. - */ -public enum LogEventType { - /** - * Connector was successfully updated, and changes were incorporated - */ - CONNECTOR_UPDATED, - - /** - * Connector went online - */ - CONNECTOR_STATUS_CHANGE_ONLINE, - - /** - * Connector went offline - */ - CONNECTOR_STATUS_CHANGE_OFFLINE, - - /** - * Connector was "force deleted" - */ - CONNECTOR_STATUS_CHANGE_FORCE_DELETED, - - /** - * Contract Offer was updated - */ - CONTRACT_OFFER_UPDATED, - - /** - * Contract Offer was clicked - */ - CONTRACT_OFFER_CLICK; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java new file mode 100644 index 000000000..781878185 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries; + +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorPageDbRow; +import de.sovity.edc.ext.brokerserver.dao.queries.utils.SearchUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.OrderField; +import org.jooq.impl.DSL; + +import java.util.List; +import java.util.stream.Stream; + +public class ConnectorQueries { + + public Stream findAll(DSLContext dslContext) { + return dslContext.selectFrom(Tables.CONNECTOR).stream(); + } + + public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { + var c = Tables.CONNECTOR; + return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); + } + + public List forConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { + var c = Tables.CONNECTOR; + var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( + c.TITLE, c.DESCRIPTION, c.ENDPOINT, c.IDS_ID, c.CONNECTOR_ID)); + return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) + .from(c) + .where(filterBySearchQuery) + .orderBy(sortConnectorPage(c, sorting)) + .fetchInto(ConnectorPageDbRow.class); + } + + @NotNull + private List> sortConnectorPage(Connector c, ConnectorPageSortingType sorting) { + var alphabetically = c.TITLE.asc(); + var recentFirst = c.CREATED_AT.desc(); + if (sorting == ConnectorPageSortingType.MOST_RECENT) { + return List.of(recentFirst, alphabetically); + } + return List.of(alphabetically, recentFirst); + } + + private Field dataOfferCount(Field endpoint) { + var d = Tables.DATA_OFFER; + return DSL.select(DSL.count()).from(d).where(d.CONNECTOR_ENDPOINT.eq(endpoint)).asField(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java new file mode 100644 index 000000000..5a76127f5 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.impl.DSL; + +/** + * Utilities for dealing with PostgreSQL Like Operation values + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class LikeUtils { + + /** + * Create LIKE condition value for "field contains word". + * + * @param field field + * @param word word + * @return "%escapedWord%" + */ + public static Condition contains(Field field, String word) { + if (StringUtils.isBlank(word)) { + return DSL.trueCondition(); + } + + return field.like("%" + escape(word) + "%"); + } + + + /** + * Escapes "\", "%", "_" in given string for a LIKE operation + * + * @param string unescaped string + * @return escaped string + */ + public static String escape(String string) { + return string.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_"); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java new file mode 100644 index 000000000..5b917d9ff --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries.utils; + +import de.sovity.edc.ext.brokerserver.utils.StringUtils2; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.util.List; + +/** + * DB Search Queries + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SearchUtils { + + /** + * Simple search + *
+ * All search query words must be contained in at least one search target. + * + * @param searchQuery search query + * @param searchTargets target fields + * @return JOOQ Condition + */ + public static Condition simpleSearch(String searchQuery, List> searchTargets) { + var words = StringUtils2.lowercaseWords(searchQuery); + return DSL.and(words.stream() + .map(word -> anySearchTargetContains(searchTargets, word)) + .toList()); + } + + private static Condition anySearchTargetContains(List> searchTargets, String word) { + return DSL.or(searchTargets.stream().map(field -> LikeUtils.contains(field, word)).toList()); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java deleted file mode 100644 index 9e2d83a27..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ConnectorQueries.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.stores; - -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import org.jooq.DSLContext; - -import java.util.stream.Stream; - -public class ConnectorQueries { - - public Stream findAll(DSLContext dslContext) { - return dslContext.selectFrom(Tables.CONNECTOR).stream(); - } - - public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { - var c = Tables.CONNECTOR; - return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java deleted file mode 100644 index 0cad9830b..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/ContractOfferStore.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.stores; - -import de.sovity.edc.ext.brokerserver.dao.models.ContractOfferRecord; -import org.apache.commons.lang3.Validate; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -public class ContractOfferStore { - private final Map contractOffersById = new HashMap<>(); - - public Stream findAll() { - return contractOffersById.values().stream(); - } - - public ContractOfferRecord findById(String contractOfferId) { - return contractOffersById.get(contractOfferId); - } - - public ContractOfferRecord save(ContractOfferRecord contractOffer) { - Validate.notBlank(contractOffer.getId(), "Need Contract Offer ID"); - return contractOffersById.put(contractOffer.getId(), contractOffer); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java deleted file mode 100644 index 0c697e763..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/stores/LogEventStore.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.stores; - -import de.sovity.edc.ext.brokerserver.dao.models.LogEventRecord; -import lombok.NonNull; -import org.apache.commons.lang3.Validate; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -public class LogEventStore { - private final Map logEntries = new HashMap<>(); - - public LogEventRecord save(@NonNull LogEventRecord logEntry) { - Validate.isTrue(logEntry.getId() == null, "ID already set!"); - var updated = logEntry.toBuilder().id(UUID.randomUUID().toString()).build(); - logEntries.put(updated.getId(), updated); - return updated; - } - - public List findAll() { - return new ArrayList<>(logEntries.values()); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java deleted file mode 100644 index 7b67028f1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerEventLogger.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.dao.models.LogEventRecord; -import de.sovity.edc.ext.brokerserver.dao.models.LogEventStatus; -import de.sovity.edc.ext.brokerserver.dao.models.LogEventType; -import de.sovity.edc.ext.brokerserver.dao.stores.LogEventStore; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorChangeTracker; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.exception.ExceptionUtils; - -import java.time.OffsetDateTime; - -/** - * Updates a single connector. - */ -@RequiredArgsConstructor -public class BrokerEventLogger { - private final LogEventStore logEventStore; - - public void logConnectorUpdateSuccess(String connectorEndpoint, ConnectorChangeTracker changes) { - var status = changes.isEmpty() ? LogEventStatus.UNCHANGED : LogEventStatus.UPDATED; - var logEntry = connectorUpdateLogEntry(connectorEndpoint, status).toBuilder() - .userMessage(changes.getLogMessage()) - .build(); - this.logEventStore.save(logEntry); - } - - public void logConnectorUpdateFailure(String connectorEndpoint, String message, Throwable exceptionOrNull) { - var logEntry = connectorUpdateLogEntry(connectorEndpoint, LogEventStatus.ERROR).toBuilder() - .userMessage(message) - .error(exceptionOrNull == null ? null : ExceptionUtils.getStackTrace(exceptionOrNull)) - .build(); - this.logEventStore.save(logEntry); - } - - private LogEventRecord connectorUpdateLogEntry(String connectorEndpoint, LogEventStatus outcome) { - return LogEventRecord.builder() - .connectorEndpoint(connectorEndpoint) - .userMessage(getConnectorUpdatedMessage(outcome)) - .type(LogEventType.CONNECTOR_UPDATED) - .createdAt(OffsetDateTime.now()) - .status(outcome) - .build(); - } - - private static String getConnectorUpdatedMessage(LogEventStatus outcome) { - return switch (outcome) { - case UNCHANGED -> "Connector is up to date"; - case UPDATED -> "Connector was updated"; - case ERROR -> "Connector update failed"; - default -> throw new IllegalArgumentException("Unknown outcome: " + outcome); - }; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index ac548f70b..195c50991 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -14,14 +14,12 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.stores.ContractOfferStore; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class CatalogApiService { - private final ContractOfferStore contractOfferStore; private final PaginationMetadataUtils paginationMetadataUtils; public CatalogPageResult catalogPage(CatalogPageQuery query) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 15780198c..6f29a51d6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -14,9 +14,9 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.models.ConnectorPageDbRow; +import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; @@ -24,10 +24,8 @@ import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingItem; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import java.util.Arrays; -import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -41,21 +39,21 @@ public ConnectorPageResult connectorPage(ConnectorPageQuery query) { return dslContextFactory.transactionResult(dsl -> { Objects.requireNonNull(query, "query must not be null"); - var connectors = connectorQueries.findAll(dsl) - .map(ConnectorApiService::buildConnectorListEntry) - .sorted(Comparator.comparing(ConnectorListEntry::getTitle)) - .toList(); + var connectorDbRows = connectorQueries.forConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); var result = new ConnectorPageResult(); result.setAvailableSortings(buildAvailableSortings()); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectors.size())); - result.setConnectors(connectors); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); + result.setConnectors(buildConnectorListEntries(connectorDbRows)); return result; }); } - @NotNull - private static ConnectorListEntry buildConnectorListEntry(ConnectorRecord it) { + private List buildConnectorListEntries(List connectorDbRows) { + return connectorDbRows.stream().map(this::buildConnectorListEntry).toList(); + } + + private ConnectorListEntry buildConnectorListEntry(ConnectorPageDbRow it) { ConnectorListEntry dto = new ConnectorListEntry(); dto.setId(it.getEndpoint()); dto.setIdsId(it.getIdsId()); @@ -66,11 +64,11 @@ private static ConnectorListEntry buildConnectorListEntry(ConnectorRecord it) { dto.setLastFetchAt(it.getLastUpdate()); dto.setOnlineStatus(getOnlineStatus(it)); dto.setOfflineSince(it.getOfflineSince()); - dto.setNumContractOffers(-1); + dto.setNumContractOffers(it.getNumDataOffers()); return dto; } - private static ConnectorOnlineStatus getOnlineStatus(ConnectorRecord it) { + private static ConnectorOnlineStatus getOnlineStatus(ConnectorPageDbRow it) { return switch (it.getOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; @@ -78,7 +76,6 @@ private static ConnectorOnlineStatus getOnlineStatus(ConnectorRecord it) { }; } - @NotNull private static List buildAvailableSortings() { return Arrays.stream(ConnectorPageSortingType.values()).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java new file mode 100644 index 000000000..1732332eb --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.logging; + +import de.sovity.edc.ext.brokerserver.utils.StringUtils2; +import lombok.NonNull; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * Helper Dto that contains User Message + Error Stack Trace to be written into + * {@link de.sovity.edc.ext.brokerserver.db.jooq.tables.BrokerEventLog}. + *
+ * This class exists so that logging exceptions has a consistent format. + * + * @param message message + * @param stackTraceOrNull stack trace + */ +public record BrokerEventErrorMessage(String message, String stackTraceOrNull) { + + public static BrokerEventErrorMessage ofMessage(@NonNull String message) { + return new BrokerEventErrorMessage(message, null); + } + + public static BrokerEventErrorMessage ofStackTrace(@NonNull String baseMessage, @NonNull Throwable cause) { + String message = baseMessage; + message = StringUtils2.removeSuffix(message, "."); + message = StringUtils2.removeSuffix(message, ":"); + message = "%s: %s".formatted(message, cause.getClass().getName()); + return new BrokerEventErrorMessage(message, ExceptionUtils.getStackTrace(cause)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java new file mode 100644 index 000000000..77cb0d999 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.logging; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.BrokerEventLogRecord; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; + +/** + * Updates a single connector. + */ +@RequiredArgsConstructor +public class BrokerEventLogger { + + public void logConnectorUpdateSuccess(DSLContext dsl, String connectorEndpoint, ConnectorChangeTracker changes) { + var logEntry = connectorUpdateEntry(dsl, connectorEndpoint); + logEntry.setEventStatus(getConnectorUpdateStatus(changes)); + logEntry.setUserMessage(changes.toString()); + logEntry.insert(); + } + + public void logConnectorUpdateFailure(DSLContext dsl, String connectorEndpoint, BrokerEventErrorMessage errorMessage) { + var logEntry = connectorUpdateEntry(dsl, connectorEndpoint); + logEntry.setEventStatus(BrokerEventStatus.ERROR); + logEntry.setUserMessage(errorMessage.message()); + logEntry.setErrorStack(errorMessage.stackTraceOrNull()); + logEntry.insert(); + } + + private BrokerEventLogRecord connectorUpdateEntry(DSLContext dsl, String connectorEndpoint) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setCreatedAt(OffsetDateTime.now()); + return logEntry; + } + + private BrokerEventStatus getConnectorUpdateStatus(ConnectorChangeTracker changes) { + if (changes.isEmpty()) { + return BrokerEventStatus.UNCHANGED; + } + + return BrokerEventStatus.OK; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java similarity index 95% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java index 2ae70f293..61328ca24 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorChangeTracker.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing; +package de.sovity.edc.ext.brokerserver.services.logging; import lombok.Getter; import lombok.Setter; @@ -47,7 +47,8 @@ public boolean isEmpty() { return selfDescriptionChanges.isEmpty(); } - public String getLogMessage() { + @Override + public String toString() { if (isEmpty()) { return "Connector is up to date."; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java index adbb40fd0..bdda72772 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java @@ -11,6 +11,7 @@ * sovity GmbH - initial API and implementation * */ + package de.sovity.edc.ext.brokerserver.services.queue; import de.sovity.edc.ext.brokerserver.services.ConnectorQueueEntry; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java index b118cca0c..6f0bce3d9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java @@ -11,6 +11,7 @@ * sovity GmbH - initial API and implementation * */ + package de.sovity.edc.ext.brokerserver.services.queue; public class ConnectorRefreshPriority { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java index 533639f5d..0609f979d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java @@ -16,7 +16,8 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventErrorMessage; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.EdcException; import org.jooq.DSLContext; @@ -35,12 +36,18 @@ public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Th connector.update(); // Log Event - Throwable stackTrace = shouldLogStacktrace(e) ? e : null; - String message = "Failed updating connector: %s".formatted(e.getMessage()); - brokerEventLogger.logConnectorUpdateFailure(connector.getEndpoint(), message, stackTrace); + brokerEventLogger.logConnectorUpdateFailure(dsl, connector.getEndpoint(), getFailureMessage(e)); } - private boolean shouldLogStacktrace(Throwable e) { + public BrokerEventErrorMessage getFailureMessage(Throwable e) { + if (isUnexpectedException(e)) { + return BrokerEventErrorMessage.ofStackTrace("Unexpected exception during connector update.", e); + } + + return BrokerEventErrorMessage.ofMessage("Failed updating connector."); + } + + private boolean isUnexpectedException(Throwable e) { return !(e instanceof EdcException); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 55045be2b..29249fc68 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -16,7 +16,8 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; import lombok.RequiredArgsConstructor; import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; import org.jooq.DSLContext; @@ -43,7 +44,7 @@ public void handleConnectorOnline( // TODO // Log Event - brokerEventLogger.logConnectorUpdateSuccess(connector.getEndpoint(), changes); + brokerEventLogger.logConnectorUpdateSuccess(dsl, connector.getEndpoint(), changes); } private static void updateConnector(ConnectorRecord connector, ConnectorSelfDescription selfDescription, ConnectorChangeTracker changes) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index b485dd37e..346d733e8 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing; -import de.sovity.edc.ext.brokerserver.dao.stores.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import lombok.RequiredArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java new file mode 100644 index 000000000..cdf454183 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.stream.Stream; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StringUtils2 { + + /** + * Removes the suffix from the given string if it ends with it. + * + * @param string string + * @param suffix suffix to remove + * @return string without suffix + */ + public static String removeSuffix(@NonNull String string, @NonNull String suffix) { + if (string.endsWith(suffix)) { + return string.substring(0, string.length() - suffix.length()); + } + return string; + } + + /** + * Splits a string into its words and returns them in lowercase. + * + * @param string string + * @return list of lowercase words + */ + public static List lowercaseWords(String string) { + if (StringUtils.isBlank(string)) { + return List.of(); + } + + return Stream.of(string.split("\\s+")) + .map(String::toLowerCase) + .filter(StringUtils::isNotBlank) + .toList(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java index 5aee1c1bb..ea31570a5 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.brokerserver.utils; import lombok.AccessLevel; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java index d4f8f6e89..31f258b5d 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java @@ -46,6 +46,8 @@ void setUp(EdcExtension extension) { void testQueryConnectors() { var result = edcClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); assertThat(result.getConnectors()).hasSize(1); - assertThat(result.getConnectors().get(0).getEndpoint()).isEqualTo("https://example.com/ids/data"); + + var connector = result.getConnectors().get(0); + assertThat(connector.getEndpoint()).isEqualTo("https://example.com/ids/data"); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java new file mode 100644 index 000000000..ebb127206 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries.utils; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class LikeUtilsTest { + @Test + void escape() { + assertThat(LikeUtils.escape("a\\b_c%d")).isEqualTo("a\\\\b\\_c\\%d"); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java new file mode 100644 index 000000000..70f415ae5 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class StringUtils2Test { + @Test + void removeSuffix_emptyStrings() { + assertThat(StringUtils2.removeSuffix("", "")).isEmpty(); + } + + @Test + void removeSuffix_emptySuffix() { + assertThat(StringUtils2.removeSuffix("test", "")).isEqualTo("test"); + } + + + @Test + void removeSuffix_withSuffix() { + assertThat(StringUtils2.removeSuffix("testabc", "abc")).isEqualTo("test"); + } + + + @Test + void removeSuffix_withoutSuffix() { + assertThat(StringUtils2.removeSuffix("test", "abc")).isEqualTo("test"); + } + + + @Test + void lowercaseWords_emptyString() { + assertThat(StringUtils2.lowercaseWords("")).isEmpty(); + } + + @Test + void lowercaseWords_blankString() { + assertThat(StringUtils2.lowercaseWords(" ")).isEmpty(); + } + + + @Test + void lowercaseWords_someWords() { + assertThat(StringUtils2.lowercaseWords(" a \n\t B a ")).isEqualTo(List.of("a", "b", "a")); + } +} From 46b9930f172da468451480d4ab27a6760baa4b10 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 25 May 2023 07:24:10 +0200 Subject: [PATCH 021/295] CatalogApiService (#69) --- .../build.gradle.kts | 2 + .../brokerserver/db/DataSourceFactory.java | 19 ++- .../brokerserver/db/DslContextFactory.java | 4 +- .../db/DslContextFactoryHijacker.java | 38 ++++++ .../ext/brokerserver/db/FlywayFactory.java | 5 + .../ext/brokerserver/db/FlywayMigrator.java | 6 + .../db/PostgresFlywayExtension.java | 6 +- .../BrokerServerExtensionContextBuilder.java | 20 ++- .../BrokerServerResourceImpl.java | 6 +- .../ext/brokerserver/dao/AssetProperty.java | 27 ++++ .../models/DataOfferContractOfferDbRow.java | 32 +++++ .../dao/models/DataOfferDbRow.java | 40 ++++++ .../dao/queries/DataOfferQueries.java | 113 ++++++++++++++++ .../services/api/AssetPropertyParser.java | 35 +++++ .../services/api/CatalogApiService.java | 75 ++++++++++- .../services/api/ConnectorApiService.java | 25 ++-- .../services/api/PolicyDtoBuilder.java | 33 +++++ .../edc/ext/brokerserver/TestUtils.java | 2 + .../edc/ext/brokerserver/db/TestDatabase.java | 34 +++++ ...estDatabaseCancelTransactionException.java | 19 +++ .../services/api/CatalogApiTest.java | 126 ++++++++++++++++++ .../{ => services/api}/ConnectorApiTest.java | 2 +- 22 files changed, 643 insertions(+), 26 deletions(-) create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java rename extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/{ => services/api}/ConnectorApiTest.java (97%) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index cc4cc6e6e..80856b8ac 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -34,6 +34,8 @@ plugins { dependencies { api("org.jooq:jooq:3.16.4") + api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") + jooqGenerator("org.postgresql:postgresql:42.6.0") flywayMigration("org.postgresql:postgresql:42.6.0") diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java index 75cefd147..09b8627f7 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java @@ -32,12 +32,29 @@ public class DataSourceFactory { private final Config config; + /** + * Create a new {@link DataSource} from EDC Config. + * + * @return {@link DataSource}. + */ public DataSource newDataSource() { var jdbcCredentials = JdbcCredentials.fromConfig(config); + return fromJdbcCredentials(jdbcCredentials); + } + + /** + * Create a new {@link DataSource} from JDBC Credentials. + *
+ * This method was extracted into a static method, so we can call it from our Test Code. + * + * @param jdbcCredentials jdbc credentials + * @return {@link DataSource} + */ + public static DataSource fromJdbcCredentials(JdbcCredentials jdbcCredentials) { return new ConnectionFactoryDataSource(() -> newConnection(jdbcCredentials)); } - private Connection newConnection(JdbcCredentials jdbcCredentials) { + private static Connection newConnection(JdbcCredentials jdbcCredentials) { try { return DriverManager.getConnection( jdbcCredentials.jdbcUrl(), diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java index 5829ab09c..4aafd44fe 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java @@ -5,6 +5,7 @@ import org.jooq.SQLDialect; import org.jooq.impl.DSL; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import javax.sql.DataSource; @@ -22,7 +23,8 @@ public class DslContextFactory { * @return new {@link DSLContext} */ public DSLContext newDslContext() { - return DSL.using(dataSource, SQLDialect.POSTGRES); + var globalDslContextForDbTests = DslContextFactoryHijacker.getParentDslContext(); + return Objects.requireNonNullElseGet(globalDslContextForDbTests, () -> DSL.using(dataSource, SQLDialect.POSTGRES)); } /** diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java new file mode 100644 index 000000000..3dd3c95e3 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java @@ -0,0 +1,38 @@ +package de.sovity.edc.ext.brokerserver.db; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.jooq.DSLContext; + +/** + * Hijack all {@link DslContextFactory}s from test code (single thread only) + */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class DslContextFactoryHijacker { + @Getter + private static DSLContext parentDslContext = null; + + /** + * Our tests currently have no access to the running extension's context, save for REST calls. + *
+ * We use this class to hack all DslContextFactories via {@link #parentDslContext}. + *
+ * If we set the {@link #parentDslContext} to one we created with a transaction we won't commit, we won't have to reset the DB between tests. + * + * @param testTransactionDslContext parent dsl context containing the parent transaction + * @param r code to run + */ + public static void withParentDslContext(DSLContext testTransactionDslContext, Runnable r) { + if (parentDslContext != null) { + throw new IllegalStateException("Tests are being run in parallel, which won't work with our current architecture."); + } + + DslContextFactoryHijacker.parentDslContext = testTransactionDslContext; + + try { + r.run(); + } finally { + DslContextFactoryHijacker.parentDslContext = null; + } + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java index 9b3bb7e3d..ca949394d 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java @@ -15,15 +15,19 @@ package de.sovity.edc.ext.brokerserver.db; import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.system.configuration.Config; import org.flywaydb.core.Flyway; import javax.sql.DataSource; +import static de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE; + /** * Quickly launch {@link Flyway} from EDC Config */ @RequiredArgsConstructor public class FlywayFactory { + private final Config config; /** * Configure and launch {@link Flyway}. @@ -35,6 +39,7 @@ public Flyway setupFlyway(DataSource dataSource) { return Flyway.configure() .baselineOnMigrate(true) .dataSource(dataSource) + .cleanDisabled(!config.getBoolean(FLYWAY_CLEAN_ENABLE, false)) .table("flyway_schema_history") .locations("classpath:db/migration") .load(); diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java index a06768a0d..92111349f 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java @@ -35,6 +35,12 @@ public class FlywayMigrator { * Run migrations and potentially run flyway repair */ public void migrateAndRepair() { + if (config.getBoolean(PostgresFlywayExtension.FLYWAY_CLEAN, false)) { + monitor.info("Cleaning database before migrations, since %s=true and %s=true.".formatted( + PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, PostgresFlywayExtension.FLYWAY_CLEAN + )); + flyway.clean(); + } try { migrate(); } catch (FlywayException e) { diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java index cc5baa112..3c07d4e1d 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java @@ -30,6 +30,10 @@ public class PostgresFlywayExtension implements ServiceExtension { public static final String JDBC_PASSWORD = "edc.datasource.default.jdbcpassword"; @Setting public static final String FLYWAY_REPAIR = "edc.flyway.repair"; + @Setting + public static final String FLYWAY_CLEAN_ENABLE = "edc.flyway.clean.enable"; + @Setting + public static final String FLYWAY_CLEAN = "edc.flyway.clean"; @Provider public DataPlaneInstanceStatements dataPlaneInstanceStatements() { @@ -49,7 +53,7 @@ public void initialize(ServiceExtensionContext context) { var dataSourceFactory = new DataSourceFactory(config); var dataSource = dataSourceFactory.newDataSource(); - var flywayFactory = new FlywayFactory(); + var flywayFactory = new FlywayFactory(config); var flyway = flywayFactory.setupFlyway(dataSource); var flywayMigrator = new FlywayMigrator(flyway, config, monitor); flywayMigrator.migrateAndRepair(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 0642cd7fe..84a068126 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -15,12 +15,15 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; +import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; +import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorSelfDescriptionFetcher; @@ -61,6 +64,7 @@ public static BrokerServerExtensionContext buildContext( RemoteMessageDispatcherRegistry dispatcherRegistry ) { // Dao + var dataOfferQueries = new DataOfferQueries(); var dataSourceFactory = new DataSourceFactory(config); var dataSource = dataSourceFactory.newDataSource(); var dslContextFactory = new DslContextFactory(dataSource); @@ -87,6 +91,9 @@ public static BrokerServerExtensionContext buildContext( dslContextFactory, monitor ); + var policyDtoBuilder = new PolicyDtoBuilder(objectMapper); + var assetPropertyParser = new AssetPropertyParser(objectMapper); + var paginationMetadataUtils = new PaginationMetadataUtils(); // Queue var connectorQueue = new ConnectorQueue(); @@ -94,16 +101,21 @@ public static BrokerServerExtensionContext buildContext( var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config, connectorQueue); // UI Capabilities - var paginationMetadataUtils = new PaginationMetadataUtils(); var catalogApiService = new CatalogApiService( - paginationMetadataUtils + paginationMetadataUtils, + dataOfferQueries, + policyDtoBuilder, + assetPropertyParser ); var connectorApiService = new ConnectorApiService( - dslContextFactory, connectorQueries, paginationMetadataUtils ); - var brokerServerResource = new BrokerServerResourceImpl(connectorApiService, catalogApiService); + var brokerServerResource = new BrokerServerResourceImpl( + dslContextFactory, + connectorApiService, + catalogApiService + ); return new BrokerServerExtensionContext(remoteMessageDispatcher, brokerServerResource, brokerServerInitializer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 696fed3eb..3fa99ad16 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver; +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; @@ -29,16 +30,17 @@ */ @RequiredArgsConstructor public class BrokerServerResourceImpl implements BrokerServerResource { + private final DslContextFactory dslContextFactory; private final ConnectorApiService connectorApiService; private final CatalogApiService catalogApiService; @Override public CatalogPageResult catalogPage(CatalogPageQuery query) { - return catalogApiService.catalogPage(query); + return dslContextFactory.transactionResult(dsl -> catalogApiService.catalogPage(dsl, query)); } @Override public ConnectorPageResult connectorPage(ConnectorPageQuery query) { - return connectorApiService.connectorPage(query); + return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorPage(dsl, query)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java new file mode 100644 index 000000000..a5fda457a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao; + + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AssetProperty { + public static final String ASSET_ID = "asset:prop:id"; + public static final String TITLE = "asset:prop:name"; + public static final String DESCRIPTION = "asset:prop:description"; + public static final String KEYWORDS = "asset:prop:keywords"; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java new file mode 100644 index 000000000..9572388b6 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DataOfferContractOfferDbRow { + String contractOfferId; + String policyJson; + OffsetDateTime createdAt; + OffsetDateTime updatedAt; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java new file mode 100644 index 000000000..336137365 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.models; + +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; +import java.util.List; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DataOfferDbRow { + String assetId; + String connectorEndpoint; + String connectorTitle; + String connectorDescription; + ConnectorOnlineStatus connectorOnlineStatus; + String assetPropertiesJson; + OffsetDateTime createdAt; + OffsetDateTime updatedAt; + OffsetDateTime offlineSinceOrLastUpdatedAt; + List contractOffers; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java new file mode 100644 index 000000000..14e5dcf1b --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries; + +import com.github.t9t.jooq.json.JsonbDSL; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.dao.models.DataOfferContractOfferDbRow; +import de.sovity.edc.ext.brokerserver.dao.models.DataOfferDbRow; +import de.sovity.edc.ext.brokerserver.dao.queries.utils.SearchUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.OrderField; +import org.jooq.impl.DSL; + +import java.util.List; + +public class DataOfferQueries { + public List forCatalogPage(DSLContext dsl, String searchQuery, CatalogPageSortingType sorting) { + var c = Tables.CONNECTOR; + var d = Tables.DATA_OFFER; + + // Asset Properties from JSON to be used in sorting / filtering + var assetId = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.ASSET_ID); + var assetTitle = DSL.coalesce(JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.TITLE), assetId); + var assetDescription = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.DESCRIPTION); + var assetKeywords = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.KEYWORDS); + + // This date should always be non-null + // It's used in the UI to display the last relevant change date of a connector + var offlineSinceOrLastUpdatedAt = DSL.coalesce( + DSL.case_(c.ONLINE_STATUS).when(ConnectorOnlineStatus.OFFLINE, c.OFFLINE_SINCE).else_(c.LAST_UPDATE), + c.CREATED_AT + ); + + var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( + assetId, + assetTitle, + assetDescription, + assetKeywords, + c.TITLE, + c.ENDPOINT + )); + + return dsl.select( + assetId.as("assetId"), + c.ENDPOINT.as("connectorEndpoint"), + c.TITLE.as("connectorTitle"), + c.DESCRIPTION.as("connectorDescription"), + c.ONLINE_STATUS.as("connectorOnlineStatus"), + d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), + d.CREATED_AT, + d.UPDATED_AT, + offlineSinceOrLastUpdatedAt.as("offlineSinceOrLastUpdatedAt"), + getContractOffers(d.CONNECTOR_ENDPOINT, d.ASSET_ID).as("contractOffers") + ) + .from(c, d) + .where( + c.ONLINE_STATUS.eq(ConnectorOnlineStatus.ONLINE), + filterBySearchQuery + ) + .orderBy(getOrderBy(sorting, c, d, assetTitle)) + .fetchInto(DataOfferDbRow.class); + } + + @NotNull + private List> getOrderBy(CatalogPageSortingType sorting, Connector c, DataOffer d, Field assetTitle) { + List> orderBy; + if (sorting == null || sorting == CatalogPageSortingType.TITLE) { + orderBy = List.of(assetTitle.asc(), c.ENDPOINT.asc()); + } else if (sorting == CatalogPageSortingType.MOST_RECENT) { + orderBy = List.of(d.CREATED_AT.desc(), c.TITLE.asc()); + } else if (sorting == CatalogPageSortingType.ORIGINATOR) { + orderBy = List.of(c.ENDPOINT.asc(), assetTitle.asc()); + } else { + throw new IllegalArgumentException("Unknown %s: %s".formatted(CatalogPageSortingType.class.getName(), sorting)); + } + return orderBy; + } + + private Field> getContractOffers(Field connectorEndpoint, Field assetId) { + var dco = Tables.DATA_OFFER_CONTRACT_OFFER; + + var query = DSL.select( + dco.CONTRACT_OFFER_ID, + dco.POLICY.cast(String.class).as("policyJson"), + dco.CREATED_AT, + dco.UPDATED_AT + ).from(dco).where( + dco.CONNECTOR_ENDPOINT.eq(connectorEndpoint), + dco.ASSET_ID.eq(assetId)).orderBy(dco.CREATED_AT.desc() + ); + + return DSL.multiset(query).convertFrom(it -> it.into(DataOfferContractOfferDbRow.class)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java new file mode 100644 index 000000000..51cb859df --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import java.util.Map; + +@RequiredArgsConstructor +public class AssetPropertyParser { + private final ObjectMapper objectMapper; + + private final TypeReference> TYPE_TOKEN = new TypeReference<>() { + }; + + @SneakyThrows + public Map parsePropertiesFromJsonString(String assetPropertiesJson) { + return objectMapper.readValue(assetPropertiesJson, TYPE_TOKEN); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index 195c50991..c4688f6ca 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -14,15 +14,86 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.brokerserver.dao.models.DataOfferContractOfferDbRow; +import de.sovity.edc.ext.brokerserver.dao.models.DataOfferDbRow; +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingItem; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilter; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOffer; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferConnectorInfo; +import de.sovity.edc.ext.wrapper.api.common.model.AssetDto; +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; @RequiredArgsConstructor public class CatalogApiService { private final PaginationMetadataUtils paginationMetadataUtils; + private final DataOfferQueries dataOfferQueries; + private final PolicyDtoBuilder policyDtoBuilder; + private final AssetPropertyParser assetPropertyParser; + + public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); + + var dataOfferDbRows = dataOfferQueries.forCatalogPage(dsl, query.getSearchQuery(), query.getSorting()); + + var result = new CatalogPageResult(); + result.setAvailableSortings(buildAvailableSortings()); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(dataOfferDbRows.size())); + result.setAvailableFilters(new CnfFilter(List.of())); + result.setDataOffers(buildDataOffers(dataOfferDbRows)); + return result; + } + + private List buildDataOffers(List dataOfferDbRows) { + return dataOfferDbRows.stream().map(this::buildDataOffer).toList(); + } + + private DataOffer buildDataOffer(DataOfferDbRow dataOfferDbRow) { + AssetDto assetDto = new AssetDto(); + assetDto.setAssetId(dataOfferDbRow.getAssetId()); + assetDto.setCreatedAt(dataOfferDbRow.getCreatedAt()); + assetDto.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferDbRow.getAssetPropertiesJson())); + + DataOfferConnectorInfo connectorInfo = new DataOfferConnectorInfo(); + connectorInfo.setEndpoint(dataOfferDbRow.getConnectorEndpoint()); + connectorInfo.setTitle(dataOfferDbRow.getConnectorTitle()); + connectorInfo.setDescription(dataOfferDbRow.getConnectorDescription()); + connectorInfo.setOnlineStatus(getOnlineStatus(dataOfferDbRow)); + connectorInfo.setOfflineSinceOrLastUpdatedAt(dataOfferDbRow.getOfflineSinceOrLastUpdatedAt()); + + DataOffer dataOffer = new DataOffer(); + dataOffer.setAsset(assetDto); + dataOffer.setConnectorInfo(connectorInfo); + dataOffer.setPolicy(buildPolicies(dataOfferDbRow)); + return dataOffer; + } + + private List buildPolicies(DataOfferDbRow dataOfferDbRow) { + return dataOfferDbRow.getContractOffers().stream() + .map(DataOfferContractOfferDbRow::getPolicyJson) + .map(policyDtoBuilder::buildPolicyFromJson) + .toList(); + } + + private ConnectorOnlineStatus getOnlineStatus(DataOfferDbRow dataOfferDbRow) { + return switch (dataOfferDbRow.getConnectorOnlineStatus()) { + case ONLINE -> ConnectorOnlineStatus.ONLINE; + case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + dataOfferDbRow.getConnectorOnlineStatus()); + }; + } - public CatalogPageResult catalogPage(CatalogPageQuery query) { - throw new IllegalStateException("Not implemented yet"); + private static List buildAvailableSortings() { + return Arrays.stream(CatalogPageSortingType.values()).map(it -> new CatalogPageSortingItem(it, it.getTitle())).toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 6f29a51d6..cb10af2de 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -16,7 +16,6 @@ import de.sovity.edc.ext.brokerserver.dao.models.ConnectorPageDbRow; import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; @@ -24,6 +23,7 @@ import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingItem; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; import java.util.Arrays; import java.util.List; @@ -31,22 +31,19 @@ @RequiredArgsConstructor public class ConnectorApiService { - private final DslContextFactory dslContextFactory; private final ConnectorQueries connectorQueries; private final PaginationMetadataUtils paginationMetadataUtils; - public ConnectorPageResult connectorPage(ConnectorPageQuery query) { - return dslContextFactory.transactionResult(dsl -> { - Objects.requireNonNull(query, "query must not be null"); + public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); - var connectorDbRows = connectorQueries.forConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); + var connectorDbRows = connectorQueries.forConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); - var result = new ConnectorPageResult(); - result.setAvailableSortings(buildAvailableSortings()); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); - result.setConnectors(buildConnectorListEntries(connectorDbRows)); - return result; - }); + var result = new ConnectorPageResult(); + result.setAvailableSortings(buildAvailableSortings()); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); + result.setConnectors(buildConnectorListEntries(connectorDbRows)); + return result; } private List buildConnectorListEntries(List connectorDbRows) { @@ -68,7 +65,7 @@ private ConnectorListEntry buildConnectorListEntry(ConnectorPageDbRow it) { return dto; } - private static ConnectorOnlineStatus getOnlineStatus(ConnectorPageDbRow it) { + private ConnectorOnlineStatus getOnlineStatus(ConnectorPageDbRow it) { return switch (it.getOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; @@ -76,7 +73,7 @@ private static ConnectorOnlineStatus getOnlineStatus(ConnectorPageDbRow it) { }; } - private static List buildAvailableSortings() { + private List buildAvailableSortings() { return Arrays.stream(ConnectorPageSortingType.values()).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java new file mode 100644 index 000000000..2fbcc1036 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.edc.policy.model.Policy; + +@RequiredArgsConstructor +public class PolicyDtoBuilder { + private final ObjectMapper objectMapper; + + @SneakyThrows + public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { + var policy = objectMapper.readValue(policyJson, Policy.class); + return new PolicyDto(policy); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 86c2ec14e..608abce54 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -44,6 +44,8 @@ public static Map createConfiguration(TestDatabase testDatabase, config.put(PostgresFlywayExtension.JDBC_URL, testDatabase.getJdbcUrl()); config.put(PostgresFlywayExtension.JDBC_USER, testDatabase.getJdbcUser()); config.put(PostgresFlywayExtension.JDBC_PASSWORD, testDatabase.getJdbcPassword()); + config.put(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, "true"); + config.put(PostgresFlywayExtension.FLYWAY_CLEAN, "true"); config.putAll(getCoreEdcJdbcConfig(testDatabase)); config.put(BrokerServerExtension.KNOWN_CONNECTORS, String.join(",", connectorEndpoints)); return config; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index 31f98c4e5..968e24791 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -14,13 +14,47 @@ package de.sovity.edc.ext.brokerserver.db; +import de.sovity.edc.ext.brokerserver.db.utils.JdbcCredentials; +import org.jooq.DSLContext; +import org.jooq.exception.DataAccessException; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; +import java.util.function.Consumer; + public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { String getJdbcUrl(); String getJdbcUser(); String getJdbcPassword(); + + /** + * New {@link DslContextFactory} from the test database's credentials + * + * @return {@link DslContextFactory} + */ + default DslContextFactory getDslContextFactory() { + var jdbcCredentials = new JdbcCredentials(getJdbcUrl(), getJdbcUser(), getJdbcPassword()); + var dataSource = DataSourceFactory.fromJdbcCredentials(jdbcCredentials); + return new DslContextFactory(dataSource); + } + + /** + * Runs given code within a test transaction. + *
+ * Globally hijacks all {@link DslContextFactory}s to use this test transaction. + * + * @param code code to run within the test transaction + */ + default void testTransaction(Consumer code) { + try { + getDslContextFactory().transaction(dsl -> DslContextFactoryHijacker.withParentDslContext(dsl, () -> { + code.accept(dsl); + throw new TestDatabaseCancelTransactionException(); + })); + } catch (TestDatabaseCancelTransactionException e) { + // Ignore + } + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java new file mode 100644 index 000000000..31126e7eb --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +public class TestDatabaseCancelTransactionException extends RuntimeException { + +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java new file mode 100644 index 000000000..fdac912b6 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.client.gen.model.CatalogPageQuery; +import de.sovity.edc.client.gen.model.DataOfferConnectorInfo; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Policy; +import org.jooq.JSONB; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class CatalogApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, List.of("https://example.com/ids/data"))); + } + + @Test + void testQueryConnectors() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setTitle("Example Connector"); + connector.setDescription("My example Connector..."); + connector.setIdsId("urn:connector:my-connector"); + connector.setConnectorId("http://my-connector"); + connector.setEndpoint("http://my-connector/ids/data"); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setCreatedAt(today.minusDays(1)); + connector.setLastUpdate(today); + connector.setOfflineSince(null); + connector.insert(); + + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + dataOffer.setAssetId("urn:artifact:my-asset"); + dataOffer.setAssetProperties(JSONB.jsonb("{\"asset:prop:id\": \"urn:artifact:my-asset\", \"asset:prop:name\": \"my-asset\"}")); + dataOffer.setConnectorEndpoint("http://my-connector/ids/data"); + dataOffer.setCreatedAt(today.minusDays(5)); + dataOffer.setUpdatedAt(today); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + contractOffer.setContractOfferId("my-contract-offer-1"); + contractOffer.setConnectorEndpoint("http://my-connector/ids/data"); + contractOffer.setAssetId("urn:artifact:my-asset"); + contractOffer.setCreatedAt(today.minusDays(5)); + contractOffer.setUpdatedAt(today); + contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.insert(); + + + var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + assertThat(result.getDataOffers()).hasSize(1); + + var dataOfferResult = result.getDataOffers().get(0); + assertThat(dataOfferResult.getConnectorInfo().getDescription()).isEqualTo("My example Connector..."); + assertThat(dataOfferResult.getConnectorInfo().getEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(dataOfferResult.getConnectorInfo().getOfflineSinceOrLastUpdatedAt()).isEqualTo(today); + assertThat(dataOfferResult.getConnectorInfo().getOnlineStatus()).isEqualTo(DataOfferConnectorInfo.OnlineStatusEnum.ONLINE); + assertThat(dataOfferResult.getConnectorInfo().getTitle()).isEqualTo("Example Connector"); + assertThat(dataOfferResult.getAsset().getAssetId()).isEqualTo("urn:artifact:my-asset"); + assertThat(dataOfferResult.getAsset().getProperties()).isEqualTo(Map.of( + "asset:prop:id", "urn:artifact:my-asset", + "asset:prop:name", "my-asset" + )); + assertThat(dataOfferResult.getAsset().getCreatedAt()).isEqualTo(today.minusDays(5)); + assertThat(toJson(dataOfferResult.getPolicy().get(0).getLegacyPolicy())).isEqualTo(toJson(dummyPolicy())); + }); + } + + private Policy dummyPolicy() { + return Policy.Builder.newInstance() + .assignee("Example Assignee") + .build(); + } + + private String policyToJson(Policy policy) { + return toJson(policy); + } + + private String toJson(Object o) { + return new ObjectMapper().valueToTree(o).toString(); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java similarity index 97% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java rename to extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index 31f258b5d..021f4cfcd 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver; +package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.client.gen.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.db.TestDatabase; From ee2c394a2bd578b3038bdd961cf4efddd47ccfb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 07:24:44 +0200 Subject: [PATCH 022/295] chore(deps): bump org.flywaydb.flyway from 9.19.0 to 9.19.1 (#70) Bumps org.flywaydb.flyway from 9.19.0 to 9.19.1. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 80856b8ac..96a50b501 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -26,7 +26,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.19.0" + id("org.flywaydb.flyway") version "9.19.1" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From 21706f338bb078db68938ca617121b9852efbce5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 07:29:17 +0200 Subject: [PATCH 023/295] chore(deps): bump org.projectlombok:lombok from 1.18.26 to 1.18.28 (#73) Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.26 to 1.18.28. - [Release notes](https://github.com/projectlombok/lombok/releases) - [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown) - [Commits](https://github.com/projectlombok/lombok/compare/v1.18.26...v1.18.28) --- updated-dependencies: - dependency-name: org.projectlombok:lombok dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../broker-server-postgres-flyway-jooq/build.gradle.kts | 4 ++-- extensions/broker-server/build.gradle.kts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 96a50b501..a88679ded 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -39,8 +39,8 @@ dependencies { jooqGenerator("org.postgresql:postgresql:42.6.0") flywayMigration("org.postgresql:postgresql:42.6.0") - annotationProcessor("org.projectlombok:lombok:1.18.26") - compileOnly("org.projectlombok:lombok:1.18.26") + annotationProcessor("org.projectlombok:lombok:1.18.28") + compileOnly("org.projectlombok:lombok:1.18.28") implementation("org.apache.commons:commons-lang3:3.12.0") implementation("${edcGroup}:core-spi:${edcVersion}") diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index b6e591a91..74069005b 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -14,8 +14,8 @@ val restAssured: String by project val testcontainersVersion: String by project dependencies { - annotationProcessor("org.projectlombok:lombok:1.18.26") - compileOnly("org.projectlombok:lombok:1.18.26") + annotationProcessor("org.projectlombok:lombok:1.18.28") + compileOnly("org.projectlombok:lombok:1.18.28") implementation("org.apache.commons:commons-lang3:3.12.0") implementation("${edcGroup}:control-plane-core:${edcVersion}") @@ -30,8 +30,8 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") - testAnnotationProcessor("org.projectlombok:lombok:1.18.26") - testCompileOnly("org.projectlombok:lombok:1.18.26") + testAnnotationProcessor("org.projectlombok:lombok:1.18.28") + testCompileOnly("org.projectlombok:lombok:1.18.28") testImplementation("org.assertj:assertj-core:${assertj}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("${edcGroup}:control-plane-core:${edcVersion}") From c42c4a7af564e5a73da691126d8d2a8278a5f7fb Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 26 May 2023 07:29:54 +0200 Subject: [PATCH 024/295] Quartz Schedules + ConnectorQueueFiller (#72) * feat: ConnectorQueue scheduler * feat: ConnectorQueue scheduler * feat: ConnectorQueue scheduler * feat: schedule periodic tasks with Quartz and CRON Expressions * chore: remove test cron job * fix existing connectors being re-added on startup --------- Co-authored-by: Tim Berthold Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server/build.gradle.kts | 2 + .../brokerserver/BrokerServerExtension.java | 5 +- .../BrokerServerExtensionContextBuilder.java | 26 ++++++- .../dao/queries/ConnectorQueries.java | 19 ++++- .../dao/queries/utils/PostgresqlUtils.java | 41 +++++++++++ .../services/BrokerServerInitializer.java | 50 ++----------- .../services/ConnectorCreator.java | 62 ++++++++++++++++ .../services/KnownConnectorsInitializer.java | 44 ++++++++++++ .../services/queue/ConnectorQueue.java | 16 +++-- .../{ => queue}/ConnectorQueueEntry.java | 21 ++++-- .../services/queue/ConnectorQueueFiller.java | 30 ++++++++ .../schedules/ConnectorRefreshJob.java | 32 +++++++++ .../schedules/QuartzScheduleInitializer.java | 71 +++++++++++++++++++ .../services/schedules/utils/CronJobRef.java | 39 ++++++++++ .../schedules/utils/JobFactoryImpl.java | 50 +++++++++++++ .../brokerserver/utils/CollectionUtils2.java | 40 +++++++++++ .../edc/ext/brokerserver/TestUtils.java | 7 +- .../services/api/CatalogApiTest.java | 8 +-- .../services/api/ConnectorApiTest.java | 7 +- 19 files changed, 494 insertions(+), 76 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/{ => queue}/ConnectorQueueEntry.java (59%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 74069005b..e5efe20a2 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -45,6 +45,8 @@ dependencies { testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") + + implementation("org.quartz-scheduler:quartz:2.3.2") } tasks.getByName("test") { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 1c1937c2d..fa1b8e7b1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -30,7 +30,10 @@ public class BrokerServerExtension implements ServiceExtension { public static final String EXTENSION_NAME = "BrokerServerExtension"; @Setting - public static final String KNOWN_CONNECTORS = "edc.brokerserver.known.connectors"; + public static final String KNOWN_CONNECTORS = "edc.broker.server.known.connectors"; + + @Setting + public static final String CRON_CONNECTOR_REFRESH = "edc.broker.server.cron.connector.refresh"; @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 84a068126..4b2a0f7f5 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -19,6 +19,8 @@ import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; +import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; +import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; @@ -26,6 +28,7 @@ import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorSelfDescriptionFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; @@ -33,6 +36,9 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.ContractOfferFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.sender.DescriptionRequestSender; import de.sovity.edc.ext.brokerserver.services.refreshing.sender.IdsMultipartExtendedRemoteMessageDispatcher; +import de.sovity.edc.ext.brokerserver.services.schedules.ConnectorRefreshJob; +import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; +import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; import lombok.NoArgsConstructor; import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.IdsMultipartSender; import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; @@ -43,6 +49,8 @@ import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.TypeManager; +import java.util.List; + /** * Manual Dependency Injection. @@ -94,11 +102,23 @@ public static BrokerServerExtensionContext buildContext( var policyDtoBuilder = new PolicyDtoBuilder(objectMapper); var assetPropertyParser = new AssetPropertyParser(objectMapper); var paginationMetadataUtils = new PaginationMetadataUtils(); - - // Queue var connectorQueue = new ConnectorQueue(); + var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); + var connectorCreator = new ConnectorCreator(connectorQueries); + var knownConnectorsInitializer = new KnownConnectorsInitializer(config, connectorQueue, connectorCreator); + + // Schedules + List> jobs = List.of( + new CronJobRef<>( + BrokerServerExtension.CRON_CONNECTOR_REFRESH, + ConnectorRefreshJob.class, + () -> new ConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ) + ); - var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, config, connectorQueue); + // Startup + var quartzScheduleInitializer = new QuartzScheduleInitializer(config, monitor, jobs); + var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, knownConnectorsInitializer, quartzScheduleInitializer); // UI Capabilities var catalogApiService = new CatalogApiService( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java index 781878185..b313f1b58 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.dao.queries; import de.sovity.edc.ext.brokerserver.dao.models.ConnectorPageDbRow; +import de.sovity.edc.ext.brokerserver.dao.queries.utils.PostgresqlUtils; import de.sovity.edc.ext.brokerserver.dao.queries.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; @@ -26,13 +27,15 @@ import org.jooq.OrderField; import org.jooq.impl.DSL; +import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.stream.Stream; public class ConnectorQueries { - public Stream findAll(DSLContext dslContext) { - return dslContext.selectFrom(Tables.CONNECTOR).stream(); + public Stream findAll(DSLContext dsl) { + return dsl.selectFrom(Tables.CONNECTOR).stream(); } public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { @@ -40,6 +43,18 @@ public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); } + public Set findConnectorsForScheduledRefresh(DSLContext dsl) { + var c = Tables.CONNECTOR; + return dsl.select(c.ENDPOINT).from(c).fetchSet(c.ENDPOINT); + } + + public Set findExistingConnectors(DSLContext dsl, Collection connectorEndpoints) { + var c = Tables.CONNECTOR; + return dsl.select(c.ENDPOINT).from(c) + .where(PostgresqlUtils.in(c.ENDPOINT, connectorEndpoints)) + .fetchSet(c.ENDPOINT); + } + public List forConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java new file mode 100644 index 000000000..dd321d475 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.util.Collection; + +/** + * PostgreSQL + JooQ Utils + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PostgresqlUtils { + + /** + * Replaces the IN operation with "field = ANY(...)" + * + * @param field field + * @param values values + * @return condition + */ + public static Condition in(Field field, Collection values) { + return field.eq(DSL.any(values.toArray(String[]::new))); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java index f3d7a46c0..4f1ca59f2 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java @@ -14,58 +14,18 @@ package de.sovity.edc.ext.brokerserver.services; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; -import de.sovity.edc.ext.brokerserver.utils.UrlUtils; +import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.system.configuration.Config; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; - -import java.time.OffsetDateTime; -import java.util.List; @RequiredArgsConstructor public class BrokerServerInitializer { private final DslContextFactory dslContextFactory; - private final Config config; - private final ConnectorQueue connectorQueue; + private final KnownConnectorsInitializer knownConnectorsInitializer; + private final QuartzScheduleInitializer quartzScheduleInitializer; public void onStartup() { - List connectorEndpoints = getPreconfiguredConnectorEndpoints(); - dslContextFactory.transaction(dsl -> initializeConnectorList(dsl, connectorEndpoints)); - - connectorEndpoints.forEach(it -> connectorQueue.add(new ConnectorQueueEntry(it, OffsetDateTime.now(), ConnectorRefreshPriority.ADDED_ON_STARTUP))); - } - - private void initializeConnectorList(DSLContext dsl, List connectorEndpoints) { - var connectorRecords = connectorEndpoints.stream() - .map(String::trim) - .map(this::newConnectorRow) - .toList(); - dsl.batchStore(connectorRecords).execute(); - } - - @NotNull - private ConnectorRecord newConnectorRow(String endpoint) { - var connectorId = UrlUtils.getEverythingBeforeThePath(endpoint); - - var connector = new ConnectorRecord(); - connector.setEndpoint(endpoint); - connector.setConnectorId(connectorId); - connector.setTitle("Unknown Connector"); - connector.setDescription("Awaiting initial crawling of given connector."); - connector.setIdsId(""); - connector.setCreatedAt(OffsetDateTime.now()); - connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); - return connector; - } - - private List getPreconfiguredConnectorEndpoints() { - return List.of(config.getString(BrokerServerExtension.KNOWN_CONNECTORS).split(",")); + dslContextFactory.transaction(knownConnectorsInitializer::addKnownConnectorsOnStartup); + quartzScheduleInitializer.startSchedules(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java new file mode 100644 index 000000000..aed61c597 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; +import de.sovity.edc.ext.brokerserver.utils.UrlUtils; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.util.List; + +@RequiredArgsConstructor +public class ConnectorCreator { + private final ConnectorQueries connectorQueries; + + public void addConnectors(DSLContext dsl, List connectorEndpoints) { + // Don't create connectors that already exist + var existingConnectors = connectorQueries.findExistingConnectors(dsl, connectorEndpoints); + var newConnectors = CollectionUtils2.difference(connectorEndpoints, existingConnectors); + + var connectorRecords = newConnectors.stream() + .map(String::trim) + .map(this::newConnectorRow) + .toList(); + + if (!connectorRecords.isEmpty()) { + dsl.batchStore(connectorRecords).execute(); + } + } + + @NotNull + private ConnectorRecord newConnectorRow(String endpoint) { + var connectorId = UrlUtils.getEverythingBeforeThePath(endpoint); + + var connector = new ConnectorRecord(); + connector.setEndpoint(endpoint); + connector.setConnectorId(connectorId); + connector.setTitle("Unknown Connector"); + connector.setDescription("Awaiting initial crawling of given connector."); + connector.setIdsId(""); + connector.setCreatedAt(OffsetDateTime.now()); + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + return connector; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java new file mode 100644 index 000000000..8adad301a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.system.configuration.Config; +import org.jooq.DSLContext; + +import java.util.Arrays; +import java.util.List; + +@RequiredArgsConstructor +public class KnownConnectorsInitializer { + private final Config config; + private final ConnectorQueue connectorQueue; + private final ConnectorCreator connectorCreator; + + public void addKnownConnectorsOnStartup(DSLContext dsl) { + List connectorEndpoints = getKnownConnectorsConfigValue(); + connectorCreator.addConnectors(dsl, connectorEndpoints); + connectorQueue.addAll(connectorEndpoints, ConnectorRefreshPriority.ADDED_ON_STARTUP); + } + + private List getKnownConnectorsConfigValue() { + String knownConnectorsString = config.getString(BrokerServerExtension.KNOWN_CONNECTORS, ""); + return Arrays.stream(knownConnectorsString.split(",")).map(String::trim).filter(StringUtils::isNotBlank).distinct().toList(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java index bdda72772..e76d6cf29 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java @@ -14,18 +14,20 @@ package de.sovity.edc.ext.brokerserver.services.queue; -import de.sovity.edc.ext.brokerserver.services.ConnectorQueueEntry; - +import java.util.Collection; import java.util.concurrent.PriorityBlockingQueue; public class ConnectorQueue { - private final PriorityBlockingQueue connectorQueueEntries = new PriorityBlockingQueue<>(); + private final PriorityBlockingQueue queue = new PriorityBlockingQueue<>(); - public void add(ConnectorQueueEntry entry) { - connectorQueueEntries.add(entry); + public String poll() { + return queue.poll().getEndpoint(); } - public ConnectorQueueEntry poll() { - return connectorQueueEntries.poll(); + public void addAll(Collection endpoints, int priority) { + var entries = endpoints.stream() + .map(endpoint -> new ConnectorQueueEntry(endpoint, priority)) + .toList(); + queue.addAll(entries); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueEntry.java similarity index 59% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueEntry.java index bfcbd8cf8..b73e194c5 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorQueueEntry.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueEntry.java @@ -12,19 +12,26 @@ * */ -package de.sovity.edc.ext.brokerserver.services; +package de.sovity.edc.ext.brokerserver.services.queue; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; -import java.time.OffsetDateTime; import java.util.Comparator; -public record ConnectorQueueEntry(String endpoint, - OffsetDateTime lastUpdate, - int priority) implements Comparable { + +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode(of = "endpoint", callSuper = false) +public class ConnectorQueueEntry implements Comparable { private static final Comparator COMPARATOR = Comparator - .comparing(ConnectorQueueEntry::priority) - .thenComparing(ConnectorQueueEntry::lastUpdate); + .comparing(ConnectorQueueEntry::getPriority); + + @NotNull + private final String endpoint; + private final int priority; @Override public int compareTo(@NotNull ConnectorQueueEntry connectorQueueEntry) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java new file mode 100644 index 000000000..909c8ced1 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.queue; + +import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class ConnectorQueueFiller { + private final ConnectorQueue connectorQueue; + private final ConnectorQueries connectorQueries; + + public void enqueueAllConnectors(DSLContext dsl) { + var endpoints = connectorQueries.findConnectorsForScheduledRefresh(dsl); + connectorQueue.addAll(endpoints, ConnectorRefreshPriority.SCHEDULED_REFRESH); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java new file mode 100644 index 000000000..fde62b166 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules; + +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; +import lombok.RequiredArgsConstructor; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +public class ConnectorRefreshJob implements Job { + private final DslContextFactory dslContextFactory; + private final ConnectorQueueFiller connectorQueueFiller; + + @Override + public void execute(JobExecutionContext context) { + dslContextFactory.transaction(connectorQueueFiller::enqueueAllConnectors); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java new file mode 100644 index 000000000..3e4fb294a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules; + +import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; +import de.sovity.edc.ext.brokerserver.services.schedules.utils.JobFactoryImpl; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.Scheduler; +import org.quartz.TriggerBuilder; +import org.quartz.impl.StdSchedulerFactory; + +import java.util.Collection; + +@RequiredArgsConstructor +public class QuartzScheduleInitializer { + private final Config config; + private final Monitor monitor; + private final Collection> jobs; + + @SneakyThrows + public void startSchedules() { + var jobFactory = new JobFactoryImpl(jobs); + var scheduler = StdSchedulerFactory.getDefaultScheduler(); + scheduler.setJobFactory(jobFactory); + + jobs.forEach(job -> scheduleCronJob(scheduler, job)); + scheduler.start(); + } + + @SneakyThrows + private void scheduleCronJob(Scheduler scheduler, CronJobRef cronJobRef) { + // CRON property name doubles as job name + var jobName = cronJobRef.configPropertyName(); + + // Skip scheduling if property not provided to ensure tests have no schedules running + var cronTrigger = config.getString(jobName, ""); + if (StringUtils.isBlank(cronTrigger)) { + monitor.info("No cron trigger configured for %s. Skipping.".formatted(jobName)); + return; + } + + monitor.info("Starting schedule %s=%s.".formatted(jobName, cronTrigger)); + var job = JobBuilder.newJob(cronJobRef.clazz()) + .withIdentity(jobName) + .build(); + var trigger = TriggerBuilder.newTrigger() + .withIdentity(jobName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronTrigger)) + .build(); + + scheduler.scheduleJob(job, trigger); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java new file mode 100644 index 000000000..a98ad2cd7 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules.utils; + +import org.quartz.Job; + +import java.util.function.Supplier; + +/** + * Broker Server CRON Job. + * + * @param configPropertyName EDC Config property that decides cron expression + * @param clazz class of the job + * @param factory factory that initializes the task class + * @param job type + */ +public record CronJobRef( + String configPropertyName, + Class clazz, + Supplier factory +) { + + @SuppressWarnings("unchecked") + public Supplier asJobSupplier() { + return (Supplier) factory; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java new file mode 100644 index 000000000..9e1597ae2 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules.utils; + +import lombok.NonNull; +import org.quartz.Job; +import org.quartz.Scheduler; +import org.quartz.spi.JobFactory; +import org.quartz.spi.TriggerFiredBundle; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class JobFactoryImpl implements JobFactory { + private final Map, Supplier> factories; + + public JobFactoryImpl(@NonNull Collection> jobs) { + factories = jobs.stream().collect(Collectors.toMap( + CronJobRef::clazz, + CronJobRef::asJobSupplier + )); + } + + @Override + public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) { + Class jobClazz = bundle.getJobDetail().getJobClass(); + Supplier factory = factories.get(jobClazz); + if (factory == null) { + throw new IllegalArgumentException("No factory for Job class %s. Supported Job classes are: %s.".formatted( + jobClazz.getName(), + factories.keySet().stream().map(Class::getName).collect(Collectors.joining(", ")) + )); + } + return factory.get(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java new file mode 100644 index 000000000..cdf3c2150 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CollectionUtils2 { + /** + * Set Difference + * + * @param a base set + * @param b remove these items + * @param set item type + * @return a difference b + */ + public static Set difference(@NonNull Collection a, @NonNull Collection b) { + var result = new HashSet<>(a); + result.removeAll(b); + return result; + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 608abce54..9935e720b 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -33,7 +33,10 @@ public class TestUtils { public static final String IDS_ENDPOINT = "http://localhost:" + DATA_PORT + "/api/v1/data/ids"; @NotNull - public static Map createConfiguration(TestDatabase testDatabase, List connectorEndpoints) { + public static Map createConfiguration( + TestDatabase testDatabase, + Map additionalConfigProperties + ) { Map config = new HashMap<>(); config.put("web.http.port", String.valueOf(getFreePort())); config.put("web.http.path", "/api"); @@ -47,7 +50,7 @@ public static Map createConfiguration(TestDatabase testDatabase, config.put(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, "true"); config.put(PostgresFlywayExtension.FLYWAY_CLEAN, "true"); config.putAll(getCoreEdcJdbcConfig(testDatabase)); - config.put(BrokerServerExtension.KNOWN_CONNECTORS, String.join(",", connectorEndpoints)); + config.putAll(additionalConfigProperties); return config; } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index fdac912b6..54a49bcff 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -30,13 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.List; import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; @@ -52,7 +46,7 @@ class CatalogApiTest { @BeforeEach void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, List.of("https://example.com/ids/data"))); + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); } @Test diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index 021f4cfcd..c1fba8c25 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.client.gen.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import org.eclipse.edc.junit.annotations.ApiTest; @@ -24,7 +25,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import java.util.List; +import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; @@ -39,7 +40,9 @@ class ConnectorApiTest { @BeforeEach void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, List.of("https://example.com/ids/data"))); + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( + BrokerServerExtension.KNOWN_CONNECTORS, "https://example.com/ids/data" + ))); } @Test From 150910a441977c33eab691229c85d992a03522a8 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 30 May 2023 11:46:59 +0200 Subject: [PATCH 025/295] feat: ContractOfferFetcher, DataOfferWriter (#78) * feat: ContractOfferFetcher, DataOfferWriter * fix remove self description ids message stub as it does not exist in ms9 * chore: remove dead code --- .../V2__Broker_Server_Initial_DB_Model.sql | 1 - .../brokerserver/BrokerServerExtension.java | 18 +-- .../BrokerServerExtensionContext.java | 3 - .../BrokerServerExtensionContextBuilder.java | 51 +++++--- .../dao/models/ConnectorPageDbRow.java | 1 - .../dao/queries/ConnectorQueries.java | 2 +- .../DataOfferContractOfferQueries.java | 29 +++++ .../dao/queries/DataOfferQueries.java | 6 + .../services/ConnectorCreator.java | 1 - .../services/api/ConnectorApiService.java | 2 +- .../services/queue/ConnectorQueue.java | 16 ++- .../ConnectorSelfDescriptionFetcher.java | 48 -------- .../ConnectorUpdateSuccessWriter.java | 21 ++-- .../services/refreshing/ConnectorUpdater.java | 12 +- .../refreshing/ContractOfferFetcher.java | 33 ----- .../ConnectorUnreachableException.java | 21 ++++ .../offers/ContractOfferFetcher.java | 46 +++++++ .../offers/ContractOfferRecordUpdater.java | 65 ++++++++++ .../refreshing/offers/DataOfferBuilder.java | 91 ++++++++++++++ .../refreshing/offers/DataOfferFetcher.java | 43 +++++++ .../offers/DataOfferPatchApplier.java | 50 ++++++++ .../offers/DataOfferPatchBuilder.java | 116 ++++++++++++++++++ .../offers/DataOfferRecordUpdater.java | 65 ++++++++++ .../refreshing/offers/DataOfferWriter.java | 41 +++++++ .../services/refreshing/offers/DiffUtils.java | 79 ++++++++++++ .../offers/model/DataOfferPatch.java | 65 ++++++++++ .../offers/model/FetchedDataOffer.java | 31 +++++ .../model/FetchedDataOfferContractOffer.java | 28 +++++ .../ConnectorSelfDescription.java | 4 +- .../ConnectorSelfDescriptionFetcher.java | 30 +++++ .../sender/DescriptionRequestSender.java | 65 ---------- .../sender/ExtendedMessageProtocol.java | 25 ---- ...tipartExtendedRemoteMessageDispatcher.java | 30 ----- .../message/DescriptionRequestMessage.java | 32 ----- .../edc/ext/brokerserver/utils/MapUtils.java | 18 +++ .../ext/brokerserver/utils/StreamUtils2.java | 39 ++++++ .../edc/ext/brokerserver/db/TestDatabase.java | 1 - .../services/api/CatalogApiTest.java | 1 - 38 files changed, 932 insertions(+), 298 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/{ => selfdescription}/ConnectorSelfDescription.java (70%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index be578ead0..a89275df6 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -4,7 +4,6 @@ create table connector ( endpoint text not null, connector_id text not null, - ids_id text not null, title text, description text, last_update timestamp with time zone, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index fa1b8e7b1..d5dadae91 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -15,11 +15,9 @@ package de.sovity.edc.ext.brokerserver; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; -import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; +import org.eclipse.edc.connector.spi.catalog.CatalogService; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Setting; -import org.eclipse.edc.spi.http.EdcHttpClient; -import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.spi.types.TypeManager; @@ -41,17 +39,11 @@ public class BrokerServerExtension implements ServiceExtension { @Inject private WebService webService; - @Inject - private EdcHttpClient httpClient; - - @Inject - private DynamicAttributeTokenService dynamicAttributeTokenService; - @Inject private TypeManager typeManager; @Inject - private RemoteMessageDispatcherRegistry dispatcherRegistry; + private CatalogService catalogService; /** * Manual Dependency Injection Result @@ -68,16 +60,12 @@ public void initialize(ServiceExtensionContext context) { services = BrokerServerExtensionContextBuilder.buildContext( context.getConfig(), context.getMonitor(), - httpClient, - dynamicAttributeTokenService, typeManager, - dispatcherRegistry + catalogService ); var managementApiGroup = managementApiConfiguration.getContextAlias(); webService.registerResource(managementApiGroup, services.brokerServerResource()); - - dispatcherRegistry.register(services.remoteMessageDispatcher()); } @Override diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index 01571c84f..992718b86 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -16,18 +16,15 @@ import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; -import org.eclipse.edc.spi.message.RemoteMessageDispatcher; /** * Manual Dependency Injection result * - * @param remoteMessageDispatcher IDS Message Client * @param brokerServerResource REST Resource with API Endpoint implementations * @param brokerServerInitializer Startup Logic */ public record BrokerServerExtensionContext( - RemoteMessageDispatcher remoteMessageDispatcher, BrokerServerResource brokerServerResource, BrokerServerInitializer brokerServerInitializer ) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 4b2a0f7f5..855987661 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferContractOfferQueries; import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; @@ -29,22 +30,24 @@ import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorSelfDescriptionFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.ContractOfferFetcher; -import de.sovity.edc.ext.brokerserver.services.refreshing.sender.DescriptionRequestSender; -import de.sovity.edc.ext.brokerserver.services.refreshing.sender.IdsMultipartExtendedRemoteMessageDispatcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferRecordUpdater; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferBuilder; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchApplier; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchBuilder; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; +import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescriptionFetcher; import de.sovity.edc.ext.brokerserver.services.schedules.ConnectorRefreshJob; import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; import lombok.NoArgsConstructor; -import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.IdsMultipartSender; -import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; +import org.eclipse.edc.connector.spi.catalog.CatalogService; import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.spi.http.EdcHttpClient; -import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.TypeManager; @@ -66,10 +69,8 @@ public class BrokerServerExtensionContextBuilder { public static BrokerServerExtensionContext buildContext( Config config, Monitor monitor, - EdcHttpClient httpClient, - DynamicAttributeTokenService dynamicAttributeTokenService, TypeManager typeManager, - RemoteMessageDispatcherRegistry dispatcherRegistry + CatalogService catalogService ) { // Dao var dataOfferQueries = new DataOfferQueries(); @@ -78,20 +79,30 @@ public static BrokerServerExtensionContext buildContext( var dslContextFactory = new DslContextFactory(dataSource); var connectorQueries = new ConnectorQueries(); - // IDS Message Client - var objectMapper = typeManager.getMapper(); - var idsMultipartSender = new IdsMultipartSender(monitor, httpClient, dynamicAttributeTokenService, objectMapper); - var remoteMessageDispatcher = new IdsMultipartExtendedRemoteMessageDispatcher(idsMultipartSender); - var descriptionRequestSender = new DescriptionRequestSender(); // Services - var connectorSelfDescriptionFetcher = new ConnectorSelfDescriptionFetcher(dispatcherRegistry); + var objectMapper = typeManager.getMapper(); + var connectorSelfDescriptionFetcher = new ConnectorSelfDescriptionFetcher(); var brokerEventLogger = new BrokerEventLogger(); - var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger); + var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); + var dataOfferRecordUpdater = new DataOfferRecordUpdater(); + var dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); + var dataOfferPatchBuilder = new DataOfferPatchBuilder( + dataOfferContractOfferQueries, + dataOfferQueries, + dataOfferRecordUpdater, + contractOfferRecordUpdater + ); + var dataOfferPatchApplier = new DataOfferPatchApplier(); + var dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); + var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger, dataOfferWriter); var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger); - var contractOfferFetcher = new ContractOfferFetcher(); + var contractOfferFetcher = new ContractOfferFetcher(catalogService); + var fetchedDataOfferBuilder = new DataOfferBuilder(objectMapper); + var dataOfferFetcher = new DataOfferFetcher(contractOfferFetcher, fetchedDataOfferBuilder); var connectorUpdater = new ConnectorUpdater( connectorSelfDescriptionFetcher, + dataOfferFetcher, connectorUpdateSuccessWriter, connectorUpdateFailureWriter, contractOfferFetcher, @@ -136,6 +147,6 @@ public static BrokerServerExtensionContext buildContext( connectorApiService, catalogApiService ); - return new BrokerServerExtensionContext(remoteMessageDispatcher, brokerServerResource, brokerServerInitializer); + return new BrokerServerExtensionContext(brokerServerResource, brokerServerInitializer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java index a7664ca13..1c88c29a9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java @@ -28,7 +28,6 @@ public class ConnectorPageDbRow { String endpoint; String connectorId; - String idsId; String title; String description; OffsetDateTime lastUpdate; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java index b313f1b58..4ef1a2c41 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java @@ -58,7 +58,7 @@ public Set findExistingConnectors(DSLContext dsl, Collection con public List forConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( - c.TITLE, c.DESCRIPTION, c.ENDPOINT, c.IDS_ID, c.CONNECTOR_ID)); + c.TITLE, c.DESCRIPTION, c.ENDPOINT, c.CONNECTOR_ID)); return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) .from(c) .where(filterBySearchQuery) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java new file mode 100644 index 000000000..2d415fa99 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import org.jooq.DSLContext; + +import java.util.List; + +public class DataOfferContractOfferQueries { + + public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { + var co = Tables.DATA_OFFER_CONTRACT_OFFER; + return dsl.selectFrom(co).where(co.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java index 14e5dcf1b..970108152 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java @@ -23,6 +23,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -110,4 +111,9 @@ private Field> getContractOffers(Field return DSL.multiset(query).convertFrom(it -> it.into(DataOfferContractOfferDbRow.class)); } + + public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { + var d = Tables.DATA_OFFER; + return dsl.selectFrom(d).where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index aed61c597..9af41346d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -54,7 +54,6 @@ private ConnectorRecord newConnectorRow(String endpoint) { connector.setConnectorId(connectorId); connector.setTitle("Unknown Connector"); connector.setDescription("Awaiting initial crawling of given connector."); - connector.setIdsId(""); connector.setCreatedAt(OffsetDateTime.now()); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); return connector; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index cb10af2de..513a6cbd4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -53,7 +53,7 @@ private List buildConnectorListEntries(List queue = new PriorityBlockingQueue<>(); - public String poll() { - return queue.poll().getEndpoint(); + /** + * Get the next item. Waits by blocking current thread. + * + * @return the next item + * @throws InterruptedException on thread interruption + */ + public String take() throws InterruptedException { + return queue.take().getEndpoint(); } + /** + * Enqueues connectors for update. + * + * @param endpoints connector endpoints + * @param priority priority from {@link ConnectorRefreshPriority} + */ public void addAll(Collection endpoints, int priority) { var entries = endpoints.stream() .map(endpoint -> new ConnectorQueueEntry(endpoint, priority)) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java deleted file mode 100644 index 64f038e3a..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescriptionFetcher.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing; - -import de.sovity.edc.ext.brokerserver.services.refreshing.sender.message.DescriptionRequestMessage; -import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; - -import java.net.MalformedURLException; -import java.net.URL; - -/** - * Fetch Connector Metadata. - */ -@RequiredArgsConstructor -public class ConnectorSelfDescriptionFetcher { - private static final String CONTEXT_SD_REQUEST = "SelfDescriptionRequest"; - - private final RemoteMessageDispatcherRegistry dispatcherRegistry; - - public ConnectorSelfDescription fetch(String connectorEndpoint) { - try { - var connectorEndpointUrl = new URL(connectorEndpoint); - var descriptionRequestMessage = new DescriptionRequestMessage(connectorEndpointUrl); - var descriptionResponse = dispatcherRegistry.send(String.class, descriptionRequestMessage, () -> CONTEXT_SD_REQUEST).get(); - - // TODO parse self-description - return new ConnectorSelfDescription("TODO", "TODO", descriptionResponse); - } catch (MalformedURLException e) { - throw new EdcException("Invalid connector-endpoint URL", e); - } catch (Exception e) { - throw new EdcException("Failed to fetch connector self-description", e); - } - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 29249fc68..583210f75 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -18,47 +18,46 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescription; import lombok.RequiredArgsConstructor; -import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; import org.jooq.DSLContext; import java.time.OffsetDateTime; -import java.util.List; +import java.util.Collection; import java.util.Objects; @RequiredArgsConstructor public class ConnectorUpdateSuccessWriter { private final BrokerEventLogger brokerEventLogger; + private final DataOfferWriter dataOfferWriter; public void handleConnectorOnline( DSLContext dsl, ConnectorRecord connector, ConnectorSelfDescription selfDescription, - List contractOffers + Collection dataOffers ) { // Track changes for final log message ConnectorChangeTracker changes = new ConnectorChangeTracker(); updateConnector(connector, selfDescription, changes); - // Update contract offers (if changed) - // TODO + // Update data offers + dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), dataOffers); // Log Event brokerEventLogger.logConnectorUpdateSuccess(dsl, connector.getEndpoint(), changes); } private static void updateConnector(ConnectorRecord connector, ConnectorSelfDescription selfDescription, ConnectorChangeTracker changes) { - if (!Objects.equals(selfDescription.idsId(), connector.getIdsId())) { - changes.addSelfDescriptionChange("IDS Connector ID"); - connector.setIdsId(selfDescription.idsId()); - } if (!Objects.equals(selfDescription.title(), connector.getTitle())) { changes.addSelfDescriptionChange("Title"); - connector.setIdsId(selfDescription.title()); + connector.setTitle(selfDescription.title()); } if (!Objects.equals(selfDescription.description(), connector.getDescription())) { changes.addSelfDescriptionChange("Description"); - connector.setIdsId(selfDescription.description()); + connector.setDescription(selfDescription.description()); } connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setLastUpdate(OffsetDateTime.now()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index 346d733e8..585df6bdc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -17,18 +17,20 @@ import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescription; +import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescriptionFetcher; import lombok.RequiredArgsConstructor; -import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; import org.eclipse.edc.spi.monitor.Monitor; -import java.util.List; - /** * Updates a single connector. */ @RequiredArgsConstructor public class ConnectorUpdater { private final ConnectorSelfDescriptionFetcher connectorSelfDescriptionFetcher; + private final DataOfferFetcher dataOfferFetcher; private final ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter; private final ConnectorUpdateFailureWriter connectorUpdateFailureWriter; private final ContractOfferFetcher contractOfferFetcher; @@ -44,12 +46,12 @@ public class ConnectorUpdater { public void updateConnector(String connectorEndpoint) { try { ConnectorSelfDescription selfDescription = connectorSelfDescriptionFetcher.fetch(connectorEndpoint); - List contractOffers = contractOfferFetcher.fetch(connectorEndpoint); + var dataOffers = dataOfferFetcher.fetch(connectorEndpoint); // Update connector in a single transaction dslContextFactory.transaction(dsl -> { ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); - connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, selfDescription, contractOffers); + connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, selfDescription, dataOffers); }); } catch (Exception e) { try { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java deleted file mode 100644 index fd4eccc95..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ContractOfferFetcher.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing; - -import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; - -import java.util.List; - -public class ContractOfferFetcher { - - /** - * Fetches Connector contract offers - * - * @param connectorEndpoint connector endpoint - * @return updated connector db row - */ - public List fetch(String connectorEndpoint) { - // TODO implement - throw new IllegalArgumentException("Not yet implemented"); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java new file mode 100644 index 000000000..de9172f2a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.exceptions; + +public class ConnectorUnreachableException extends RuntimeException { + public ConnectorUnreachableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java new file mode 100644 index 000000000..fcdd370e1 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.services.refreshing.exceptions.ConnectorUnreachableException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.util.List; + +@RequiredArgsConstructor +public class ContractOfferFetcher { + private final CatalogService catalogService; + + /** + * Fetches Connector contract offers + * + * @param connectorEndpoint connector endpoint + * @return updated connector db row + */ + @SneakyThrows + public List fetch(String connectorEndpoint) { + try { + return catalogService.getByProviderUrl(connectorEndpoint, QuerySpec.max()).get().getContractOffers(); + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new ConnectorUnreachableException("Failed to fetch connector contract offers", e); + } + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java new file mode 100644 index 000000000..f8b53803a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import lombok.RequiredArgsConstructor; +import org.jooq.JSONB; + +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Creates or updates {@link DataOfferContractOfferRecord} DB Rows. + *

+ * (Or at least prepares them for batch inserts / updates) + */ +@RequiredArgsConstructor +public class ContractOfferRecordUpdater { + + /** + * Create new {@link DataOfferContractOfferRecord} from {@link FetchedDataOfferContractOffer}. + * + * @param dataOffer parent data offer db row + * @param fetchedContractOffer fetched contract offer + * @return new db row + */ + public DataOfferContractOfferRecord newContractOffer(DataOfferRecord dataOffer, FetchedDataOfferContractOffer fetchedContractOffer) { + var contractOffer = new DataOfferContractOfferRecord(); + contractOffer.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); + contractOffer.setAssetId(dataOffer.getAssetId()); + contractOffer.setCreatedAt(OffsetDateTime.now()); + updateContractOffer(contractOffer, fetchedContractOffer); + return null; + } + + /** + * Update existing {@link DataOfferContractOfferRecord} with changes from {@link FetchedDataOfferContractOffer}. + * + * @param contractOffer existing row + * @param fetchedContractOffer changes to be integrated + * @return if anything was changed + */ + public boolean updateContractOffer(DataOfferContractOfferRecord contractOffer, FetchedDataOfferContractOffer fetchedContractOffer) { + if (!Objects.equals(contractOffer.getPolicy().data(), fetchedContractOffer.getPolicyJson())) { + contractOffer.setPolicy(JSONB.jsonb(fetchedContractOffer.getPolicyJson())); + contractOffer.setUpdatedAt(OffsetDateTime.now()); + return true; + } + return false; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java new file mode 100644 index 000000000..4e5e7747e --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import de.sovity.edc.ext.brokerserver.utils.StreamUtils2; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; + +import static java.util.stream.Collectors.groupingBy; + +@RequiredArgsConstructor +public class DataOfferBuilder { + private final ObjectMapper objectMapper; + + /** + * De-duplicates {@link ContractOffer}s into {@link FetchedDataOffer}s. + *

+ * Also de-duplicates {@link ContractOffer}s into {@link FetchedDataOfferContractOffer}s. + * + * @param contractOffers {@link ContractOffer}s + * @return {@link FetchedDataOffer}s + */ + public Collection deduplicateContractOffers(Collection contractOffers) { + return groupByAssetId(contractOffers) + .stream() + .map(offers -> buildFetchedDataOffer(offers.get(0).getAsset(), offers)) + .toList(); + } + + @NotNull + private FetchedDataOffer buildFetchedDataOffer(Asset asset, List offers) { + var dataOffer = new FetchedDataOffer(); + dataOffer.setAssetId(asset.getId()); + dataOffer.setAssetPropertiesJson(getAssetPropertiesJson(asset)); + dataOffer.setContractOffers(buildFetchedDataOfferContractOffers(offers)); + return dataOffer; + } + + @NotNull + private List buildFetchedDataOfferContractOffers(List offers) { + return offers.stream() + .map(this::buildFetchedDataOfferContractOffer) + .filter(StreamUtils2.distinctByKey(FetchedDataOfferContractOffer::getContractOfferId)) + .toList(); + } + + @NotNull + private FetchedDataOfferContractOffer buildFetchedDataOfferContractOffer(ContractOffer offer) { + var contractOffer = new FetchedDataOfferContractOffer(); + contractOffer.setContractOfferId(offer.getId()); + contractOffer.setPolicyJson(getPolicyJson(offer)); + return contractOffer; + } + + private Collection> groupByAssetId(Collection contractOffers) { + return contractOffers.stream().collect(groupingBy(offer -> offer.getAsset().getId())).values(); + } + + @NotNull + @SneakyThrows + private String getAssetPropertiesJson(Asset asset) { + return objectMapper.writeValueAsString(asset.getProperties()); + } + + @NotNull + @SneakyThrows + private String getPolicyJson(ContractOffer offer) { + return objectMapper.writeValueAsString(offer.getPolicy()); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java new file mode 100644 index 000000000..d87705716 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; + +import java.util.Collection; + +@RequiredArgsConstructor +public class DataOfferFetcher { + private final ContractOfferFetcher contractOfferFetcher; + private final DataOfferBuilder dataOfferBuilder; + + /** + * Fetches {@link ContractOffer}s and de-duplicates them into {@link FetchedDataOffer}s. + * + * @param connectorEndpoint connector endpoint + * @return updated connector db row + */ + @SneakyThrows + public Collection fetch(String connectorEndpoint) { + // Contract Offers contain assets multiple times, with different policies + var contractOffers = contractOfferFetcher.fetch(connectorEndpoint); + + // Data Offers represent unique assets + return dataOfferBuilder.deduplicateContractOffers(contractOffers); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java new file mode 100644 index 000000000..91cb0395f --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.DataOfferPatch; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class DataOfferPatchApplier { + + @SneakyThrows + public void writeDataOfferPatch(DSLContext dsl, DataOfferPatch dataOfferPatch) { + if (!dataOfferPatch.getDataOffersToUpdate().isEmpty()) { + dsl.batchUpdate(dataOfferPatch.getDataOffersToUpdate()).execute(); + } + if (!dataOfferPatch.getContractOffersToUpdate().isEmpty()) { + dsl.batchUpdate(dataOfferPatch.getContractOffersToUpdate()).execute(); + } + + // insert: parent entity first + if (!dataOfferPatch.getDataOffersToInsert().isEmpty()) { + dsl.batchInsert(dataOfferPatch.getDataOffersToInsert()).execute(); + } + if (!dataOfferPatch.getContractOffersToInsert().isEmpty()) { + dsl.batchInsert(dataOfferPatch.getContractOffersToInsert()).execute(); + } + + // delete: child entity first + if (!dataOfferPatch.getContractOffersToDelete().isEmpty()) { + dsl.batchDelete(dataOfferPatch.getContractOffersToDelete()).execute(); + } + if (!dataOfferPatch.getDataOffersToDelete().isEmpty()) { + dsl.batchDelete(dataOfferPatch.getDataOffersToDelete()).execute(); + } + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java new file mode 100644 index 000000000..8e56e37ca --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.DataOfferPatch; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.Collection; +import java.util.List; + +import static java.util.stream.Collectors.groupingBy; + +@RequiredArgsConstructor +public class DataOfferPatchBuilder { + private final DataOfferContractOfferQueries dataOfferContractOfferQueries; + private final DataOfferQueries dataOfferQueries; + private final DataOfferRecordUpdater dataOfferRecordUpdater; + private final ContractOfferRecordUpdater contractOfferRecordUpdater; + + /** + * Fetches existing data offers of given connector endpoint and compares them with fetched data offers. + * + * @param dsl dsl + * @param connectorEndpoint connector endpoint + * @param fetchedDataOffers fetched data offers + * @return change list / patch + */ + public DataOfferPatch buildDataOfferPatch( + DSLContext dsl, + String connectorEndpoint, + Collection fetchedDataOffers + ) { + var patch = new DataOfferPatch(); + var dataOffers = dataOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint); + var contractOffersByAssetId = dataOfferContractOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint) + .stream() + .collect(groupingBy(DataOfferContractOfferRecord::getAssetId)); + + var diff = DiffUtils.compareLists( + dataOffers, + DataOfferRecord::getAssetId, + fetchedDataOffers, + FetchedDataOffer::getAssetId + ); + + diff.added().forEach(fetched -> { + var newRecord = dataOfferRecordUpdater.newDataOffer(connectorEndpoint, fetched); + patch.insertDataOffer(newRecord); + patchContractOffers(patch, newRecord, List.of(), fetched.getContractOffers()); + }); + + diff.updated().forEach(match -> { + var existing = match.existing(); + var fetched = match.fetched(); + + if (dataOfferRecordUpdater.updateDataOffer(existing, fetched)) { + patch.updateDataOffer(existing); + } + var contractOffers = contractOffersByAssetId.getOrDefault(existing.getAssetId(), List.of()); + patchContractOffers(patch, existing, contractOffers, fetched.getContractOffers()); + }); + + diff.removed().forEach(patch::deleteDataOffer); + + return patch; + } + + private void patchContractOffers( + DataOfferPatch patch, + DataOfferRecord dataOffer, + Collection contractOffers, + Collection fetchedContractOffers + ) { + var diff = DiffUtils.compareLists( + contractOffers, + DataOfferContractOfferRecord::getContractOfferId, + fetchedContractOffers, + FetchedDataOfferContractOffer::getContractOfferId + ); + + diff.added().forEach(fetched -> { + var newRecord = contractOfferRecordUpdater.newContractOffer(dataOffer, fetched); + patch.insertContractOffer(newRecord); + }); + + diff.updated().forEach(match -> { + var existing = match.existing(); + var fetched = match.fetched(); + + if (contractOfferRecordUpdater.updateContractOffer(existing, fetched)) { + patch.updateContractOffer(existing); + } + }); + + diff.removed().forEach(patch::deleteContractOffer); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java new file mode 100644 index 000000000..338006ed7 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import lombok.RequiredArgsConstructor; +import org.jooq.JSONB; + +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Creates or updates {@link DataOfferRecord} DB Rows. + *

+ * (Or at least prepares them for batch inserts / updates) + */ +@RequiredArgsConstructor +public class DataOfferRecordUpdater { + /** + * Create a new {@link DataOfferRecord}. + * + * @param connectorEndpoint connector endpoint + * @param fetchedDataOffer new db row data + * @return new db row + */ + public DataOfferRecord newDataOffer(String connectorEndpoint, FetchedDataOffer fetchedDataOffer) { + var dataOffer = new DataOfferRecord(); + dataOffer.setConnectorEndpoint(connectorEndpoint); + dataOffer.setAssetId(fetchedDataOffer.getAssetId()); + dataOffer.setCreatedAt(OffsetDateTime.now()); + updateDataOffer(dataOffer, fetchedDataOffer); + return dataOffer; + } + + + /** + * Update existing {@link DataOfferRecord}. + * + * @param dataOffer existing row + * @param fetchedDataOffer changes to be incorporated + * @return whether any fields were updated + */ + public boolean updateDataOffer(DataOfferRecord dataOffer, FetchedDataOffer fetchedDataOffer) { + String existingAssetProps = dataOffer.getAssetProperties().data(); + String fetchedAssetProps = fetchedDataOffer.getAssetPropertiesJson(); + if (!Objects.equals(fetchedAssetProps, existingAssetProps)) { + dataOffer.setAssetProperties(JSONB.jsonb(fetchedAssetProps)); + return true; + } + return false; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java new file mode 100644 index 000000000..9d357b0d7 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.jooq.DSLContext; + +import java.util.Collection; + +@RequiredArgsConstructor +public class DataOfferWriter { + private final DataOfferPatchBuilder dataOfferPatchBuilder; + private final DataOfferPatchApplier dataOfferPatchApplier; + + /** + * Updates a connector's data offers with given {@link FetchedDataOffer}s. + * + * @param dsl dsl + * @param connectorEndpoint connector endpoint + * @param fetchedDataOffers fetched data offers + */ + @SneakyThrows + public void updateDataOffers(DSLContext dsl, String connectorEndpoint, Collection fetchedDataOffers) { + var patch = dataOfferPatchBuilder.buildDataOfferPatch(dsl, connectorEndpoint, fetchedDataOffers); + dataOfferPatchApplier.writeDataOfferPatch(dsl, patch); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java new file mode 100644 index 000000000..8d9b68ef9 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java @@ -0,0 +1,79 @@ +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.utils.MapUtils; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.function.Function; + + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DiffUtils { + /** + * Tries to match two collections by a key, then collects planned change sets as {@link DiffResult}. + * + * @param existing list of existing elements + * @param existingKeyFn existing elements key extractor + * @param fetched list of fetched elements + * @param fetchedKeyFn fetched elements key extractor + * @param first collection type + * @param second collection type + * @param key type + */ + public static DiffResult compareLists( + Collection existing, + Function existingKeyFn, + Collection fetched, + Function fetchedKeyFn + ) { + var existingByKey = MapUtils.associateBy(existing, existingKeyFn); + var fetchedByKey = MapUtils.associateBy(fetched, fetchedKeyFn); + + var keys = new HashSet<>(existingByKey.keySet()); + keys.addAll(fetchedByKey.keySet()); + + var result = new DiffResult(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + + keys.forEach(key -> { + var existingItem = existingByKey.get(key); + var fetchedItem = fetchedByKey.get(key); + + if (existingItem == null) { + result.added.add(fetchedItem); + } else if (fetchedItem == null) { + result.removed.add(existingItem); + } else { + result.updated.add(new DiffResultMatch<>(existingItem, fetchedItem)); + } + }); + + return result; + } + + /** + * Result of comparing two collections by keys. + * + * @param added elements that are present in fetched collection but not in existing + * @param updated elements that are present in both collections + * @param removed elements that are present in existing collection but not in fetched + * @param existing item type + * @param fetched item type + */ + record DiffResult(List added, List> updated, List removed) { + } + + /** + * Pair of elements that are present in both collections. + * + * @param existing existing item + * @param fetched fetched item + * @param existing item type + * @param fetched item type + */ + record DiffResultMatch(A existing, B fetched) { + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java new file mode 100644 index 000000000..e9bf5ad69 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; + +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains planned DB Row changes to be applied as batch. + */ +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DataOfferPatch { + List dataOffersToInsert = new ArrayList<>(); + List dataOffersToUpdate = new ArrayList<>(); + List dataOffersToDelete = new ArrayList<>(); + + List contractOffersToInsert = new ArrayList<>(); + List contractOffersToUpdate = new ArrayList<>(); + List contractOffersToDelete = new ArrayList<>(); + + public void insertDataOffer(DataOfferRecord offer) { + dataOffersToInsert.add(offer); + } + + public void updateDataOffer(DataOfferRecord offer) { + dataOffersToUpdate.add(offer); + } + + public void deleteDataOffer(DataOfferRecord offer) { + dataOffersToDelete.add(offer); + } + + public void insertContractOffer(DataOfferContractOfferRecord offer) { + contractOffersToInsert.add(offer); + } + + public void updateContractOffer(DataOfferContractOfferRecord offer) { + contractOffersToUpdate.add(offer); + } + + public void deleteContractOffer(DataOfferContractOfferRecord offer) { + contractOffersToDelete.add(offer); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java new file mode 100644 index 000000000..5391e455d --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class FetchedDataOffer { + String assetId; + String assetPropertiesJson; + List contractOffers; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java new file mode 100644 index 000000000..76c099dfa --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class FetchedDataOfferContractOffer { + String contractOfferId; + String policyJson; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java similarity index 70% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java index a0c5ef0c9..e974a9d79 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorSelfDescription.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing; +package de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription; -public record ConnectorSelfDescription(String idsId, String title, String description) { +public record ConnectorSelfDescription(String title, String description) { } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java new file mode 100644 index 000000000..95348cd4c --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription; + +import lombok.RequiredArgsConstructor; + +/** + * Fetch Connector Metadata. + */ +@RequiredArgsConstructor +public class ConnectorSelfDescriptionFetcher { + public ConnectorSelfDescription fetch(String connectorEndpoint) { + return new ConnectorSelfDescription( + "Unknown Connector", + "As of Core EDC Milestone 9 connector self-descriptions are not supported. The connector was successfully crawled, but there is no connector metadata / description available." + ); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java deleted file mode 100644 index 1820cd9c6..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/DescriptionRequestSender.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.sender; - -import de.fraunhofer.iais.eis.DescriptionRequestMessageBuilder; -import de.fraunhofer.iais.eis.DescriptionResponseMessage; -import de.fraunhofer.iais.eis.DynamicAttributeToken; -import de.fraunhofer.iais.eis.Message; -import de.sovity.edc.ext.brokerserver.services.refreshing.sender.message.DescriptionRequestMessage; -import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.MultipartSenderDelegate; -import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.response.IdsMultipartParts; -import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.response.MultipartResponse; -import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; -import org.eclipse.edc.protocol.ids.util.CalendarUtil; - -import java.net.URI; -import java.util.List; - -import static org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.util.ResponseUtil.parseMultipartStringResponse; -import static org.eclipse.edc.protocol.ids.jsonld.JsonLd.getObjectMapper; - -public class DescriptionRequestSender implements MultipartSenderDelegate { - @Override - public Message buildMessageHeader(DescriptionRequestMessage request, DynamicAttributeToken token) throws Exception { - return new DescriptionRequestMessageBuilder() - ._modelVersion_(IdsConstants.INFORMATION_MODEL_VERSION) - ._issued_(CalendarUtil.gregorianNow()) - ._securityToken_(token) - ._issuerConnector_(new URI(request.getConnectorAddress())) - ._senderAgent_(new URI(request.getConnectorAddress())) - .build(); - } - - @Override - public String buildMessagePayload(DescriptionRequestMessage request) throws Exception { - return null; - } - - @Override - public MultipartResponse getResponseContent(IdsMultipartParts parts) throws Exception { - return parseMultipartStringResponse(parts, getObjectMapper()); - } - - @Override - public List> getAllowedResponseTypes() { - return List.of(DescriptionResponseMessage.class); - } - - @Override - public Class getMessageType() { - return DescriptionRequestMessage.class; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java deleted file mode 100644 index 4c1262ba0..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/ExtendedMessageProtocol.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.sender; - -import org.eclipse.edc.protocol.ids.spi.types.MessageProtocol; - -public final class ExtendedMessageProtocol { - private static final String EXTENDED_SUFFIX = "-extended"; - public static final String IDS_EXTENDED_PROTOCOL = String.format("%s%s", MessageProtocol.IDS_MULTIPART, EXTENDED_SUFFIX); - - private ExtendedMessageProtocol() { - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java deleted file mode 100644 index 1bb844166..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/IdsMultipartExtendedRemoteMessageDispatcher.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.sender; - -import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.IdsMultipartRemoteMessageDispatcher; -import org.eclipse.edc.protocol.ids.api.multipart.dispatcher.sender.IdsMultipartSender; - -public class IdsMultipartExtendedRemoteMessageDispatcher extends IdsMultipartRemoteMessageDispatcher { - - public IdsMultipartExtendedRemoteMessageDispatcher(IdsMultipartSender idsMultipartSender) { - super(idsMultipartSender); - } - - @Override - public String protocol() { - return ExtendedMessageProtocol.IDS_EXTENDED_PROTOCOL; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java deleted file mode 100644 index 5c3377781..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/sender/message/DescriptionRequestMessage.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.sender.message; - -import de.sovity.edc.ext.brokerserver.services.refreshing.sender.ExtendedMessageProtocol; -import org.eclipse.edc.spi.types.domain.message.RemoteMessage; - -import java.net.URL; - -public record DescriptionRequestMessage(URL connectorEndpoint) implements RemoteMessage { - @Override - public String getProtocol() { - return ExtendedMessageProtocol.IDS_EXTENDED_PROTOCOL; - } - - @Override - public String getConnectorAddress() { - return connectorEndpoint.toString(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java new file mode 100644 index 000000000..4dfc770db --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java @@ -0,0 +1,18 @@ +package de.sovity.edc.ext.brokerserver.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapUtils { + public static Map associateBy(Collection collection, Function keyExtractor) { + return collection.stream().collect(Collectors.toMap(keyExtractor, Function.identity(), (a, b) -> { + throw new IllegalStateException("Duplicate key %s.".formatted(keyExtractor.apply(a))); + })); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java new file mode 100644 index 000000000..8a5701286 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.function.Function; +import java.util.function.Predicate; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StreamUtils2 { + + /** + * Returns a predicate that filters out all elements that have the same key as a previous element. + * + * @param keyFn key extractor + * @param item type + * @param key type + * @return predicate to be used in {@link java.util.stream.Stream#filter(Predicate)} + */ + public static Predicate distinctByKey(Function keyFn) { + var keys = new HashSet<>(); + return t -> keys.add(keyFn.apply(t)); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index 968e24791..e5dab25b4 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -16,7 +16,6 @@ import de.sovity.edc.ext.brokerserver.db.utils.JdbcCredentials; import org.jooq.DSLContext; -import org.jooq.exception.DataAccessException; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 54a49bcff..45770f169 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -58,7 +58,6 @@ void testQueryConnectors() { var connector = dsl.newRecord(Tables.CONNECTOR); connector.setTitle("Example Connector"); connector.setDescription("My example Connector..."); - connector.setIdsId("urn:connector:my-connector"); connector.setConnectorId("http://my-connector"); connector.setEndpoint("http://my-connector/ids/data"); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); From 65b6836b8beddbcc23105e8e9559a1a10e461bab Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 30 May 2023 14:58:47 +0200 Subject: [PATCH 026/295] fix data source config properties (#80) --- .../edc/ext/brokerserver/db/PostgresFlywayExtension.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java index 3c07d4e1d..e4107037c 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java @@ -25,9 +25,9 @@ public class PostgresFlywayExtension implements ServiceExtension { @Setting(required = true) public static final String JDBC_URL = "edc.datasource.default.url"; @Setting(required = true) - public static final String JDBC_USER = "edc.datasource.default.jdbcuser"; + public static final String JDBC_USER = "edc.datasource.default.user"; @Setting(required = true) - public static final String JDBC_PASSWORD = "edc.datasource.default.jdbcpassword"; + public static final String JDBC_PASSWORD = "edc.datasource.default.password"; @Setting public static final String FLYWAY_REPAIR = "edc.flyway.repair"; @Setting From 91bb49171664be5294a4f80226afe8252a498a74 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 31 May 2023 07:35:45 +0200 Subject: [PATCH 027/295] feat: docker-compose.yaml (#81) --- .env | 4 ++ docker-compose.yaml | 109 +++++++++++++++++++++++++++++++++----------- 2 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 000000000..cff794e01 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# Config for docker-compose.yaml +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:3.2.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity5 diff --git a/docker-compose.yaml b/docker-compose.yaml index 7327f72c7..8a521c23e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,40 +1,39 @@ version: "3.8" services: - edc: - build: - dockerfile: connector/Dockerfile + broker-ui: + image: ${EDC_UI_IMAGE} + ports: + - '11000:80' + environment: + - EDC_UI_ACTIVE_PROFILE=broker + - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:11002/api/v1/management + - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue + - EDC_BROKER_SERVER_KNOWN_CONNECTORS=http://connector:11003/api/v1/ids/data + broker: + image: ${BROKER_IMAGE} depends_on: - - postgresql + - broker-postgresql + - connector environment: - MY_EDC_NAME_KEBAB_CASE: "example-connector" - MY_EDC_TITLE: "EDC Connector" - MY_EDC_DESCRIPTION: "Community Edition EDC Connector by sovity" - MY_EDC_CURATOR_URL: "https://example.com" - MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_NAME_KEBAB_CASE: "broker" + MY_EDC_TITLE: "MDS Broker" + MY_EDC_DESCRIPTION: "Mobility Data Space Broker, collects and indexes all data offers of the Mobility Data Space (MDS)." + MY_EDC_CURATOR_URL: "https://mobility-dataspace.eu/" + MY_EDC_CURATOR_NAME: "Mobility Data Space" MY_EDC_MAINTAINER_URL: "https://sovity.de" MY_EDC_MAINTAINER_NAME: "sovity GmbH" # Data Management API Key EDC_API_AUTH_KEY: ApiKeyDefaultValue - # Data Space Configuration - EDC_OAUTH_CLIENT_ID: - EDC_OAUTH_TOKEN_URL: https://daps.test.mobility-dataspace.eu/token - EDC_OAUTH_PROVIDER_JWKS_URL: https://daps.test.mobility-dataspace.eu/jwks.json - EDC_BROKER_BASE_URL: https://broker.test.mobility-dataspace.eu - EDC_CLEARINGHOUSE_LOG_URL: https://clearing.test.mobility-dataspace.eu/messages/log - EDC_KEYSTORE: /secrets/keystore.jks - EDC_KEYSTORE_PASSWORD: password - EDC_OAUTH_CERTIFICATE_ALIAS: 1 - EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 - MY_EDC_PROTOCOL: "http://" - MY_EDC_FQDN: "edc" - MY_EDC_IDS_BASE_URL: "http://edc:11003" + MY_EDC_FQDN: "broker" + MY_EDC_IDS_BASE_URL: "http://broker:11003" - MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc + MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc + EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' @@ -44,18 +43,74 @@ services: - '11003:11003' - '11004:11004' - '11005:5005' + broker-postgresql: + image: docker.io/bitnami/postgresql:15 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + volumes: + - 'broker-postgresql:/bitnami/postgresql' + + connector-ui: + image: ${EDC_UI_IMAGE} + ports: + - '22000:80' + environment: + - EDC_UI_ACTIVE_PROFILE=mds-open-source + - EDC_UI_CONFIG_URL=edc-ui-config + - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:22002/api/v1/management + - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue + connector: + image: ${EDC_CE_IMAGE} + depends_on: + - connector-postgresql + environment: + MY_EDC_NAME_KEBAB_CASE: "example-connector" + MY_EDC_TITLE: "EDC Connector" + MY_EDC_DESCRIPTION: "MDS Community Edition EDC Connector" + MY_EDC_CURATOR_URL: "https://example.com" + MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_MAINTAINER_URL: "https://sovity.de" + MY_EDC_MAINTAINER_NAME: "sovity GmbH" + + # Data Management API Key + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_PROTOCOL: "http://" + MY_EDC_FQDN: "connector" + MY_EDC_IDS_BASE_URL: "http://connector:11003" + + MY_EDC_JDBC_URL: jdbc:postgresql://connector-postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' + EDC_WEB_REST_CORS_ORIGINS: '*' + ports: + - '22001:11001' + - '22002:11002' + - '22003:11003' + - '22004:11004' + - '22005:5005' volumes: - ./docs/getting-started/secrets:/secrets - postgresql: - image: docker.io/bitnami/postgresql:11 + connector-postgresql: + image: docker.io/bitnami/postgresql:15 restart: always environment: POSTGRESQL_USERNAME: edc POSTGRESQL_PASSWORD: edc POSTGRESQL_DATABASE: edc + ports: + - '54322:5432' volumes: - - 'postgresql:/bitnami/postgresql' + - 'connector-postgresql:/bitnami/postgresql' volumes: - postgresql: + broker-postgresql: + driver: local + connector-postgresql: driver: local From da8b0aafea88c9d2ec3130577dd6aa2e93c03665 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 07:36:31 +0200 Subject: [PATCH 028/295] chore(deps): bump org.testcontainers:postgresql from 1.18.1 to 1.18.2 (#84) Bumps [org.testcontainers:postgresql](https://github.com/testcontainers/testcontainers-java) from 1.18.1 to 1.18.2. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.18.1...1.18.2) --- updated-dependencies: - dependency-name: org.testcontainers:postgresql dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index a88679ded..0653d4104 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -21,7 +21,7 @@ val postgresVersion: String by project buildscript { dependencies { - classpath("org.testcontainers:postgresql:1.18.1") + classpath("org.testcontainers:postgresql:1.18.2") } } From 74100faf66e01570d41f630cdefadba8e59cda28 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 31 May 2023 08:37:01 +0200 Subject: [PATCH 029/295] feat: ConnectorRefreshExecutorPool (#75) * feat: ConnectorRefreshExecutorPool * feat: ConnectorRefreshExecutorPool * chore: merge main * feat: ConnectorRefreshExecutorPool * feat: ConnectorRefreshExecutorPool * feat: ConnectorRefreshExecutorPool * chore: checkstyle --- .../brokerserver/BrokerServerExtension.java | 3 ++ .../BrokerServerExtensionContextBuilder.java | 4 +- .../services/queue/ConnectorQueue.java | 25 +++++------- .../services/queue/ThreadPool.java | 39 +++++++++++++++++++ ...torQueueEntry.java => ThreadPoolTask.java} | 21 +++++----- .../services/refreshing/ConnectorUpdater.java | 1 + 6 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/{ConnectorQueueEntry.java => ThreadPoolTask.java} (53%) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index d5dadae91..86eea228e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -33,6 +33,9 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String CRON_CONNECTOR_REFRESH = "edc.broker.server.cron.connector.refresh"; + @Setting + public static final String NUM_THREADS = "edc.broker.server.num.threads"; + @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 855987661..96ab68c78 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -30,6 +30,7 @@ import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.brokerserver.services.queue.ThreadPool; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; @@ -113,7 +114,8 @@ public static BrokerServerExtensionContext buildContext( var policyDtoBuilder = new PolicyDtoBuilder(objectMapper); var assetPropertyParser = new AssetPropertyParser(objectMapper); var paginationMetadataUtils = new PaginationMetadataUtils(); - var connectorQueue = new ConnectorQueue(); + var threadPool = new ThreadPool(config); + var connectorQueue = new ConnectorQueue(connectorUpdater, threadPool); var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); var connectorCreator = new ConnectorCreator(connectorQueries); var knownConnectorsInitializer = new KnownConnectorsInitializer(config, connectorQueue, connectorCreator); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java index b3d4757fc..28944efe6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java @@ -14,21 +14,15 @@ package de.sovity.edc.ext.brokerserver.services.queue; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; +import lombok.RequiredArgsConstructor; + import java.util.Collection; -import java.util.concurrent.PriorityBlockingQueue; +@RequiredArgsConstructor public class ConnectorQueue { - private final PriorityBlockingQueue queue = new PriorityBlockingQueue<>(); - - /** - * Get the next item. Waits by blocking current thread. - * - * @return the next item - * @throws InterruptedException on thread interruption - */ - public String take() throws InterruptedException { - return queue.take().getEndpoint(); - } + private final ConnectorUpdater connectorUpdater; + private final ThreadPool threadPool; /** * Enqueues connectors for update. @@ -37,9 +31,8 @@ public String take() throws InterruptedException { * @param priority priority from {@link ConnectorRefreshPriority} */ public void addAll(Collection endpoints, int priority) { - var entries = endpoints.stream() - .map(endpoint -> new ConnectorQueueEntry(endpoint, priority)) - .toList(); - queue.addAll(entries); + for (String endpoint : endpoints) { + threadPool.execute(priority, () -> connectorUpdater.updateConnector(endpoint)); + } } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java new file mode 100644 index 000000000..5119a3ee8 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.queue; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; + +public class ThreadPool { + private final PriorityBlockingQueue queue; + + public ThreadPool(Config config) { + this.queue = new PriorityBlockingQueue<>(); + var threadPoolExecutor = new ThreadPoolExecutor(1, + config.getInteger(BrokerServerExtension.NUM_THREADS, 1), + 60, + java.util.concurrent.TimeUnit.SECONDS, + queue); + threadPoolExecutor.prestartAllCoreThreads(); + } + + public void execute(int priority, Runnable runnable) { + queue.add(new ThreadPoolTask(priority, runnable)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueEntry.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java similarity index 53% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueEntry.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java index b73e194c5..f7bfcd619 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueEntry.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java @@ -14,27 +14,24 @@ package de.sovity.edc.ext.brokerserver.services.queue; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; -import java.util.Comparator; - @Getter @RequiredArgsConstructor -@EqualsAndHashCode(of = "endpoint", callSuper = false) -public class ConnectorQueueEntry implements Comparable { - private static final Comparator COMPARATOR = Comparator - .comparing(ConnectorQueueEntry::getPriority); - - @NotNull - private final String endpoint; +public class ThreadPoolTask implements Comparable, Runnable { private final int priority; + private final Runnable task; + + @Override + public int compareTo(@NotNull ThreadPoolTask threadPoolTask) { + return priority - threadPoolTask.priority; + } @Override - public int compareTo(@NotNull ConnectorQueueEntry connectorQueueEntry) { - return COMPARATOR.compare(this, connectorQueueEntry); + public void run() { + this.task.run(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index 585df6bdc..fc4077aa6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -45,6 +45,7 @@ public class ConnectorUpdater { */ public void updateConnector(String connectorEndpoint) { try { + monitor.info("Updating connector: " + connectorEndpoint); ConnectorSelfDescription selfDescription = connectorSelfDescriptionFetcher.fetch(connectorEndpoint); var dataOffers = dataOfferFetcher.fetch(connectorEndpoint); From 91fd89b98ed5991115a63aeee17c4c32176cdc9d Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 31 May 2023 10:57:26 +0200 Subject: [PATCH 030/295] finalize initial broker server model (#85) * chore: edc-extensions snapshots now always update * chore: finalize initial broker server model * chore: checkstyle --------- Co-authored-by: Tim Berthold --- .../V2__Broker_Server_Initial_DB_Model.sql | 14 ++--- extensions/broker-server/build.gradle.kts | 6 +- .../BrokerServerExtensionContextBuilder.java | 4 -- .../dao/models/ConnectorPageDbRow.java | 8 +-- .../dao/models/DataOfferDbRow.java | 8 +-- .../dao/queries/ConnectorQueries.java | 13 +++-- .../dao/queries/DataOfferQueries.java | 15 ++--- .../services/ConnectorCreator.java | 2 - .../services/api/CatalogApiService.java | 55 ++++++++++--------- .../services/api/ConnectorApiService.java | 7 +-- .../ConnectorUpdateFailureWriter.java | 3 +- .../ConnectorUpdateSuccessWriter.java | 24 ++------ .../services/refreshing/ConnectorUpdater.java | 8 +-- .../services/refreshing/offers/DiffUtils.java | 14 +++++ .../ConnectorSelfDescription.java | 18 ------ .../ConnectorSelfDescriptionFetcher.java | 30 ---------- .../edc/ext/brokerserver/utils/MapUtils.java | 14 +++++ .../services/api/CatalogApiTest.java | 24 ++++---- .../services/api/ConnectorApiTest.java | 2 + 19 files changed, 109 insertions(+), 160 deletions(-) delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index a89275df6..ca6318756 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -2,14 +2,12 @@ create type connector_online_status as enum ('ONLINE', 'OFFLINE'); create table connector ( - endpoint text not null, - connector_id text not null, - title text, - description text, - last_update timestamp with time zone, - offline_since timestamp with time zone, - created_at timestamp with time zone not null, - online_status connector_online_status not null, + endpoint text not null, + connector_id text not null, + created_at timestamp with time zone not null, + last_refresh_attempt_at timestamp with time zone, + last_successful_refresh_at timestamp with time zone, + online_status connector_online_status not null, PRIMARY KEY (endpoint) ); diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index e5efe20a2..5a0893849 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -13,6 +13,10 @@ val sovityEdcExtensionsVersion: String by project val restAssured: String by project val testcontainersVersion: String by project +configurations.all { + resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) +} + dependencies { annotationProcessor("org.projectlombok:lombok:1.18.28") compileOnly("org.projectlombok:lombok:1.18.28") @@ -26,7 +30,7 @@ dependencies { implementation("${edcGroup}:ids-jsonld-serdes:${edcVersion}") api(project(":extensions:broker-server-postgres-flyway-jooq")) - api("${sovityEdcGroup}:wrapper-broker-api:${sovityEdcExtensionsVersion}") + api("${sovityEdcGroup}:wrapper-broker-api:${sovityEdcExtensionsVersion}") { isChanging = true } implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 96ab68c78..d2b6d994b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -42,7 +42,6 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchBuilder; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; -import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescriptionFetcher; import de.sovity.edc.ext.brokerserver.services.schedules.ConnectorRefreshJob; import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; @@ -83,7 +82,6 @@ public static BrokerServerExtensionContext buildContext( // Services var objectMapper = typeManager.getMapper(); - var connectorSelfDescriptionFetcher = new ConnectorSelfDescriptionFetcher(); var brokerEventLogger = new BrokerEventLogger(); var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); var dataOfferRecordUpdater = new DataOfferRecordUpdater(); @@ -102,11 +100,9 @@ public static BrokerServerExtensionContext buildContext( var fetchedDataOfferBuilder = new DataOfferBuilder(objectMapper); var dataOfferFetcher = new DataOfferFetcher(contractOfferFetcher, fetchedDataOfferBuilder); var connectorUpdater = new ConnectorUpdater( - connectorSelfDescriptionFetcher, dataOfferFetcher, connectorUpdateSuccessWriter, connectorUpdateFailureWriter, - contractOfferFetcher, connectorQueries, dslContextFactory, monitor diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java index 1c88c29a9..6a64fd22e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java @@ -28,11 +28,9 @@ public class ConnectorPageDbRow { String endpoint; String connectorId; - String title; - String description; - OffsetDateTime lastUpdate; - OffsetDateTime offlineSince; - OffsetDateTime createdAt; + private OffsetDateTime createdAt; + private OffsetDateTime lastSuccessfulRefreshAt; + private OffsetDateTime lastRefreshAttemptAt; ConnectorOnlineStatus onlineStatus; Integer numDataOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java index 336137365..6f49ebe4c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java @@ -28,13 +28,11 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class DataOfferDbRow { String assetId; - String connectorEndpoint; - String connectorTitle; - String connectorDescription; - ConnectorOnlineStatus connectorOnlineStatus; String assetPropertiesJson; OffsetDateTime createdAt; OffsetDateTime updatedAt; - OffsetDateTime offlineSinceOrLastUpdatedAt; List contractOffers; + String connectorEndpoint; + ConnectorOnlineStatus connectorOnlineStatus; + OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java index 4ef1a2c41..448c93b3b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java @@ -57,8 +57,7 @@ public Set findExistingConnectors(DSLContext dsl, Collection con public List forConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; - var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( - c.TITLE, c.DESCRIPTION, c.ENDPOINT, c.CONNECTOR_ID)); + var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of(c.ENDPOINT, c.CONNECTOR_ID)); return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) .from(c) .where(filterBySearchQuery) @@ -68,12 +67,16 @@ public List forConnectorPage(DSLContext dsl, String searchQu @NotNull private List> sortConnectorPage(Connector c, ConnectorPageSortingType sorting) { - var alphabetically = c.TITLE.asc(); + var alphabetically = c.ENDPOINT.asc(); var recentFirst = c.CREATED_AT.desc(); - if (sorting == ConnectorPageSortingType.MOST_RECENT) { + + if (sorting == null || sorting == ConnectorPageSortingType.TITLE) { + return List.of(alphabetically, recentFirst); + } else if (sorting == ConnectorPageSortingType.MOST_RECENT) { return List.of(recentFirst, alphabetically); } - return List.of(alphabetically, recentFirst); + + throw new IllegalArgumentException("Unhandled sorting type: " + sorting); } private Field dataOfferCount(Field endpoint) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java index 970108152..35f32114f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java @@ -47,7 +47,7 @@ public List forCatalogPage(DSLContext dsl, String searchQuery, C // This date should always be non-null // It's used in the UI to display the last relevant change date of a connector var offlineSinceOrLastUpdatedAt = DSL.coalesce( - DSL.case_(c.ONLINE_STATUS).when(ConnectorOnlineStatus.OFFLINE, c.OFFLINE_SINCE).else_(c.LAST_UPDATE), + c.LAST_SUCCESSFUL_REFRESH_AT, c.CREATED_AT ); @@ -56,21 +56,18 @@ public List forCatalogPage(DSLContext dsl, String searchQuery, C assetTitle, assetDescription, assetKeywords, - c.TITLE, c.ENDPOINT )); return dsl.select( assetId.as("assetId"), - c.ENDPOINT.as("connectorEndpoint"), - c.TITLE.as("connectorTitle"), - c.DESCRIPTION.as("connectorDescription"), - c.ONLINE_STATUS.as("connectorOnlineStatus"), d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), d.CREATED_AT, d.UPDATED_AT, - offlineSinceOrLastUpdatedAt.as("offlineSinceOrLastUpdatedAt"), - getContractOffers(d.CONNECTOR_ENDPOINT, d.ASSET_ID).as("contractOffers") + getContractOffers(d.CONNECTOR_ENDPOINT, d.ASSET_ID).as("contractOffers"), + c.ENDPOINT.as("connectorEndpoint"), + c.ONLINE_STATUS.as("connectorOnlineStatus"), + offlineSinceOrLastUpdatedAt.as("connectorOfflineSinceOrLastUpdatedAt") ) .from(c, d) .where( @@ -87,7 +84,7 @@ private List> getOrderBy(CatalogPageSortingType sorting, Connector if (sorting == null || sorting == CatalogPageSortingType.TITLE) { orderBy = List.of(assetTitle.asc(), c.ENDPOINT.asc()); } else if (sorting == CatalogPageSortingType.MOST_RECENT) { - orderBy = List.of(d.CREATED_AT.desc(), c.TITLE.asc()); + orderBy = List.of(d.CREATED_AT.desc(), c.ENDPOINT.asc()); } else if (sorting == CatalogPageSortingType.ORIGINATOR) { orderBy = List.of(c.ENDPOINT.asc(), assetTitle.asc()); } else { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index 9af41346d..4842f129f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -52,8 +52,6 @@ private ConnectorRecord newConnectorRow(String endpoint) { var connector = new ConnectorRecord(); connector.setEndpoint(endpoint); connector.setConnectorId(connectorId); - connector.setTitle("Unknown Connector"); - connector.setDescription("Awaiting initial crawling of given connector."); connector.setCreatedAt(OffsetDateTime.now()); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); return connector; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index c4688f6ca..bb9e70d0b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -23,10 +23,8 @@ import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilter; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOffer; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferConnectorInfo; -import de.sovity.edc.ext.wrapper.api.common.model.AssetDto; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferListEntry; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferListEntryContractOffer; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -50,41 +48,44 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { result.setAvailableSortings(buildAvailableSortings()); result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(dataOfferDbRows.size())); result.setAvailableFilters(new CnfFilter(List.of())); - result.setDataOffers(buildDataOffers(dataOfferDbRows)); + result.setDataOffers(buildDataOfferListEntries(dataOfferDbRows)); return result; } - private List buildDataOffers(List dataOfferDbRows) { - return dataOfferDbRows.stream().map(this::buildDataOffer).toList(); + private List buildDataOfferListEntries(List dataOfferDbRows) { + return dataOfferDbRows.stream() + .map(this::buildDataOfferListEntry) + .toList(); } - private DataOffer buildDataOffer(DataOfferDbRow dataOfferDbRow) { - AssetDto assetDto = new AssetDto(); - assetDto.setAssetId(dataOfferDbRow.getAssetId()); - assetDto.setCreatedAt(dataOfferDbRow.getCreatedAt()); - assetDto.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferDbRow.getAssetPropertiesJson())); - - DataOfferConnectorInfo connectorInfo = new DataOfferConnectorInfo(); - connectorInfo.setEndpoint(dataOfferDbRow.getConnectorEndpoint()); - connectorInfo.setTitle(dataOfferDbRow.getConnectorTitle()); - connectorInfo.setDescription(dataOfferDbRow.getConnectorDescription()); - connectorInfo.setOnlineStatus(getOnlineStatus(dataOfferDbRow)); - connectorInfo.setOfflineSinceOrLastUpdatedAt(dataOfferDbRow.getOfflineSinceOrLastUpdatedAt()); - - DataOffer dataOffer = new DataOffer(); - dataOffer.setAsset(assetDto); - dataOffer.setConnectorInfo(connectorInfo); - dataOffer.setPolicy(buildPolicies(dataOfferDbRow)); + private DataOfferListEntry buildDataOfferListEntry(DataOfferDbRow dataOfferDbRow) { + var dataOffer = new DataOfferListEntry(); + dataOffer.setAssetId(dataOfferDbRow.getAssetId()); + dataOffer.setCreatedAt(dataOfferDbRow.getCreatedAt()); + dataOffer.setUpdatedAt(dataOfferDbRow.getUpdatedAt()); + dataOffer.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferDbRow.getAssetPropertiesJson())); + dataOffer.setContractOffers(buildDataOfferListEntryContractOffers(dataOfferDbRow)); + dataOffer.setConnectorEndpoint(dataOfferDbRow.getConnectorEndpoint()); + dataOffer.setConnectorOfflineSinceOrLastUpdatedAt(dataOfferDbRow.getConnectorOfflineSinceOrLastUpdatedAt()); + dataOffer.setConnectorOnlineStatus(getOnlineStatus(dataOfferDbRow)); return dataOffer; } - private List buildPolicies(DataOfferDbRow dataOfferDbRow) { + private List buildDataOfferListEntryContractOffers(DataOfferDbRow dataOfferDbRow) { return dataOfferDbRow.getContractOffers().stream() - .map(DataOfferContractOfferDbRow::getPolicyJson) - .map(policyDtoBuilder::buildPolicyFromJson) + .map(this::buildDataOfferListEntryContractOffer) .toList(); } + private DataOfferListEntryContractOffer buildDataOfferListEntryContractOffer(DataOfferContractOfferDbRow contractOfferDbRow) { + var contractOffer = new DataOfferListEntryContractOffer(); + contractOffer.setContractOfferId(contractOfferDbRow.getContractOfferId()); + contractOffer.setContractPolicy(policyDtoBuilder.buildPolicyFromJson(contractOfferDbRow.getPolicyJson())); + contractOffer.setCreatedAt(contractOfferDbRow.getCreatedAt()); + contractOffer.setUpdatedAt(contractOfferDbRow.getUpdatedAt()); + return contractOffer; + } + private ConnectorOnlineStatus getOnlineStatus(DataOfferDbRow dataOfferDbRow) { return switch (dataOfferDbRow.getConnectorOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 513a6cbd4..94e906d80 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -53,14 +53,11 @@ private List buildConnectorListEntries(List dataOffers ) { + OffsetDateTime now = OffsetDateTime.now(); + // Track changes for final log message ConnectorChangeTracker changes = new ConnectorChangeTracker(); - updateConnector(connector, selfDescription, changes); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setLastSuccessfulRefreshAt(now); + connector.setLastRefreshAttemptAt(now); + connector.update(); // Update data offers dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), dataOffers); @@ -50,18 +52,4 @@ public void handleConnectorOnline( brokerEventLogger.logConnectorUpdateSuccess(dsl, connector.getEndpoint(), changes); } - private static void updateConnector(ConnectorRecord connector, ConnectorSelfDescription selfDescription, ConnectorChangeTracker changes) { - if (!Objects.equals(selfDescription.title(), connector.getTitle())) { - changes.addSelfDescriptionChange("Title"); - connector.setTitle(selfDescription.title()); - } - if (!Objects.equals(selfDescription.description(), connector.getDescription())) { - changes.addSelfDescriptionChange("Description"); - connector.setDescription(selfDescription.description()); - } - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setLastUpdate(OffsetDateTime.now()); - connector.setOfflineSince(null); - connector.update(); - } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index fc4077aa6..07d2cc505 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -17,10 +17,7 @@ import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; -import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescription; -import de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription.ConnectorSelfDescriptionFetcher; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.monitor.Monitor; @@ -29,11 +26,9 @@ */ @RequiredArgsConstructor public class ConnectorUpdater { - private final ConnectorSelfDescriptionFetcher connectorSelfDescriptionFetcher; private final DataOfferFetcher dataOfferFetcher; private final ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter; private final ConnectorUpdateFailureWriter connectorUpdateFailureWriter; - private final ContractOfferFetcher contractOfferFetcher; private final ConnectorQueries connectorQueries; private final DslContextFactory dslContextFactory; private final Monitor monitor; @@ -46,13 +41,12 @@ public class ConnectorUpdater { public void updateConnector(String connectorEndpoint) { try { monitor.info("Updating connector: " + connectorEndpoint); - ConnectorSelfDescription selfDescription = connectorSelfDescriptionFetcher.fetch(connectorEndpoint); var dataOffers = dataOfferFetcher.fetch(connectorEndpoint); // Update connector in a single transaction dslContextFactory.transaction(dsl -> { ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); - connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, selfDescription, dataOffers); + connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, dataOffers); }); } catch (Exception e) { try { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java index 8d9b68ef9..5b9bb7537 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.brokerserver.services.refreshing.offers; import de.sovity.edc.ext.brokerserver.utils.MapUtils; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java deleted file mode 100644 index e974a9d79..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescription.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription; - -public record ConnectorSelfDescription(String title, String description) { -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java deleted file mode 100644 index 95348cd4c..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/selfdescription/ConnectorSelfDescriptionFetcher.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.selfdescription; - -import lombok.RequiredArgsConstructor; - -/** - * Fetch Connector Metadata. - */ -@RequiredArgsConstructor -public class ConnectorSelfDescriptionFetcher { - public ConnectorSelfDescription fetch(String connectorEndpoint) { - return new ConnectorSelfDescription( - "Unknown Connector", - "As of Core EDC Milestone 9 connector self-descriptions are not supported. The connector was successfully crawled, but there is no connector metadata / description available." - ); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java index 4dfc770db..f43b31e56 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.brokerserver.utils; import lombok.AccessLevel; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 45770f169..5e86fff55 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -16,7 +16,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.client.gen.model.CatalogPageQuery; -import de.sovity.edc.client.gen.model.DataOfferConnectorInfo; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; @@ -33,6 +32,7 @@ import java.time.OffsetDateTime; import java.util.Map; +import static de.sovity.edc.client.gen.model.DataOfferListEntry.ConnectorOnlineStatusEnum.ONLINE; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; import static org.assertj.core.api.Assertions.assertThat; @@ -56,14 +56,12 @@ void testQueryConnectors() { var today = OffsetDateTime.now().withNano(0); var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setTitle("Example Connector"); - connector.setDescription("My example Connector..."); connector.setConnectorId("http://my-connector"); connector.setEndpoint("http://my-connector/ids/data"); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setCreatedAt(today.minusDays(1)); - connector.setLastUpdate(today); - connector.setOfflineSince(null); + connector.setLastRefreshAttemptAt(today); + connector.setLastSuccessfulRefreshAt(today); connector.insert(); var dataOffer = dsl.newRecord(Tables.DATA_OFFER); @@ -88,18 +86,16 @@ void testQueryConnectors() { assertThat(result.getDataOffers()).hasSize(1); var dataOfferResult = result.getDataOffers().get(0); - assertThat(dataOfferResult.getConnectorInfo().getDescription()).isEqualTo("My example Connector..."); - assertThat(dataOfferResult.getConnectorInfo().getEndpoint()).isEqualTo("http://my-connector/ids/data"); - assertThat(dataOfferResult.getConnectorInfo().getOfflineSinceOrLastUpdatedAt()).isEqualTo(today); - assertThat(dataOfferResult.getConnectorInfo().getOnlineStatus()).isEqualTo(DataOfferConnectorInfo.OnlineStatusEnum.ONLINE); - assertThat(dataOfferResult.getConnectorInfo().getTitle()).isEqualTo("Example Connector"); - assertThat(dataOfferResult.getAsset().getAssetId()).isEqualTo("urn:artifact:my-asset"); - assertThat(dataOfferResult.getAsset().getProperties()).isEqualTo(Map.of( + assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(dataOfferResult.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); + assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(ONLINE); + assertThat(dataOfferResult.getAssetId()).isEqualTo("urn:artifact:my-asset"); + assertThat(dataOfferResult.getProperties()).isEqualTo(Map.of( "asset:prop:id", "urn:artifact:my-asset", "asset:prop:name", "my-asset" )); - assertThat(dataOfferResult.getAsset().getCreatedAt()).isEqualTo(today.minusDays(5)); - assertThat(toJson(dataOfferResult.getPolicy().get(0).getLegacyPolicy())).isEqualTo(toJson(dummyPolicy())); + assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); + assertThat(toJson(dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy())).isEqualTo(toJson(dummyPolicy())); }); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index c1fba8c25..b3aeff9de 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -27,6 +27,7 @@ import java.util.Map; +import static de.sovity.edc.client.gen.model.ConnectorListEntry.OnlineStatusEnum.OFFLINE; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; import static org.assertj.core.api.Assertions.assertThat; @@ -52,5 +53,6 @@ void testQueryConnectors() { var connector = result.getConnectors().get(0); assertThat(connector.getEndpoint()).isEqualTo("https://example.com/ids/data"); + assertThat(connector.getOnlineStatus()).isEqualTo(OFFLINE); } } From 23780405678f793dc19a1d6d99ee01d9f7a3d6f4 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 31 May 2023 11:07:37 +0200 Subject: [PATCH 031/295] feat: upgrade to ms9 (#86) --- .../services/refreshing/offers/DataOfferBuilder.java | 9 ++++----- gradle.properties | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java index 4e5e7747e..9402271fe 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java @@ -44,15 +44,14 @@ public class DataOfferBuilder { public Collection deduplicateContractOffers(Collection contractOffers) { return groupByAssetId(contractOffers) .stream() - .map(offers -> buildFetchedDataOffer(offers.get(0).getAsset(), offers)) + .map(offers -> buildFetchedDataOffer(offers.get(0).getAssetId(), offers)) .toList(); } @NotNull - private FetchedDataOffer buildFetchedDataOffer(Asset asset, List offers) { + private FetchedDataOffer buildFetchedDataOffer(String assetId, List offers) { var dataOffer = new FetchedDataOffer(); - dataOffer.setAssetId(asset.getId()); - dataOffer.setAssetPropertiesJson(getAssetPropertiesJson(asset)); + dataOffer.setAssetId(assetId); dataOffer.setContractOffers(buildFetchedDataOfferContractOffers(offers)); return dataOffer; } @@ -74,7 +73,7 @@ private FetchedDataOfferContractOffer buildFetchedDataOfferContractOffer(Contrac } private Collection> groupByAssetId(Collection contractOffers) { - return contractOffers.stream().collect(groupingBy(offer -> offer.getAsset().getId())).values(); + return contractOffers.stream().collect(groupingBy(ContractOffer::getAssetId)).values(); } @NotNull diff --git a/gradle.properties b/gradle.properties index cce85f653..c59cbdfd8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ sovityEdcGroup=de.sovity.edc # Eclipse EDC edcGroup=org.eclipse.edc -edcVersion=0.0.1-20230220.patch1 +edcVersion=0.0.1-milestone-9 # Other Dependencies assertj=3.23.1 From 5cdad804852e9691b69b19381e05e4a2a60f0c25 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 1 Jun 2023 09:36:32 +0200 Subject: [PATCH 032/295] fix connector id, fix data offer query, hide offline data offers older than given age (#90) * fix connector id * fix data offers changed not being logged * feat: hide offline data offers older than given duration --- connector/.env | 3 ++ .../brokerserver/BrokerServerExtension.java | 3 ++ .../BrokerServerExtensionContextBuilder.java | 5 ++- .../dao/queries/DataOfferQueries.java | 31 +++++++++++-- .../services/BrokerServerSettings.java | 44 +++++++++++++++++++ .../services/ConnectorCreator.java | 3 +- .../services/api/ConnectorApiService.java | 2 +- .../ConnectorUpdateSuccessWriter.java | 2 +- .../refreshing/offers/DataOfferWriter.java | 7 ++- .../services/api/ConnectorApiTest.java | 1 + 10 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java diff --git a/connector/.env b/connector/.env index 77545aa7b..b4dca8621 100644 --- a/connector/.env +++ b/connector/.env @@ -4,6 +4,9 @@ # - KEY=Value will become KEY=${KEY:-"Value"}, so that ENV Vars can be overwritten by parent docker-compose.yaml. # - Watch out for escaping issues as values will be surrounded by quotes, and dollar signs must be escaped. +# Broker Server Related +EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D + # Deployment Settings MY_EDC_FQDN=missing-env-MY_EDC_FQDN MY_EDC_BASE_PATH= diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 86eea228e..099fc0659 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -36,6 +36,9 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String NUM_THREADS = "edc.broker.server.num.threads"; + @Setting + public static final String HIDE_OFFLINE_DATA_OFFERS_AFTER = "edc.broker.server.hide.offline.data.offers.after"; + @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index d2b6d994b..541d63282 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; +import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; @@ -72,8 +73,10 @@ public static BrokerServerExtensionContext buildContext( TypeManager typeManager, CatalogService catalogService ) { + var brokerServerSettings = new BrokerServerSettings(config); + // Dao - var dataOfferQueries = new DataOfferQueries(); + var dataOfferQueries = new DataOfferQueries(brokerServerSettings); var dataSourceFactory = new DataSourceFactory(config); var dataSource = dataSourceFactory.newDataSource(); var dslContextFactory = new DslContextFactory(dataSource); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java index 35f32114f..8cd86dc32 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java @@ -24,16 +24,23 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; +import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.Field; import org.jooq.OrderField; import org.jooq.impl.DSL; +import java.time.OffsetDateTime; import java.util.List; +@RequiredArgsConstructor public class DataOfferQueries { + private final BrokerServerSettings brokerServerSettings; + public List forCatalogPage(DSLContext dsl, String searchQuery, CatalogPageSortingType sorting) { var c = Tables.CONNECTOR; var d = Tables.DATA_OFFER; @@ -69,15 +76,33 @@ public List forCatalogPage(DSLContext dsl, String searchQuery, C c.ONLINE_STATUS.as("connectorOnlineStatus"), offlineSinceOrLastUpdatedAt.as("connectorOfflineSinceOrLastUpdatedAt") ) - .from(c, d) + .from(d) + .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) .where( - c.ONLINE_STATUS.eq(ConnectorOnlineStatus.ONLINE), - filterBySearchQuery + filterBySearchQuery, + onlyOnlineOrRecentlyOfflineConnectors(c) ) .orderBy(getOrderBy(sorting, c, d, assetTitle)) .fetchInto(DataOfferDbRow.class); } + @NotNull + private Condition onlyOnlineOrRecentlyOfflineConnectors(Connector c) { + var maxOfflineDuration = brokerServerSettings.getHideOfflineDataOffersAfter(); + + Condition maxOfflineDurationNotExceeded; + if (maxOfflineDuration == null) { + maxOfflineDurationNotExceeded = DSL.trueCondition(); + } else { + maxOfflineDurationNotExceeded = c.LAST_SUCCESSFUL_REFRESH_AT.greaterThan(OffsetDateTime.now().minus(maxOfflineDuration)); + } + + return DSL.or( + c.ONLINE_STATUS.eq(ConnectorOnlineStatus.ONLINE), + maxOfflineDurationNotExceeded + ); + } + @NotNull private List> getOrderBy(CatalogPageSortingType sorting, Connector c, DataOffer d, Field assetTitle) { List> orderBy; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java new file mode 100644 index 000000000..2ebdf8062 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import lombok.Getter; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.time.Duration; + +public class BrokerServerSettings { + private final Config config; + + @Getter + private final Duration hideOfflineDataOffersAfter; + + public BrokerServerSettings(Config config) { + this.config = config; + this.hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); + } + + private Duration getDurationOrNull(@NonNull String configProperty) { + String durationAsString = config.getString(configProperty, ""); + if (StringUtils.isBlank(durationAsString)) { + return null; + } + + return Duration.parse(durationAsString); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index 4842f129f..a44cdd6d7 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -47,11 +47,10 @@ public void addConnectors(DSLContext dsl, List connectorEndpoints) { @NotNull private ConnectorRecord newConnectorRow(String endpoint) { - var connectorId = UrlUtils.getEverythingBeforeThePath(endpoint); var connector = new ConnectorRecord(); connector.setEndpoint(endpoint); - connector.setConnectorId(connectorId); + connector.setConnectorId(UrlUtils.getEverythingBeforeThePath(endpoint)); connector.setCreatedAt(OffsetDateTime.now()); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); return connector; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 94e906d80..8a4ee63dc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -52,7 +52,7 @@ private List buildConnectorListEntries(List fetchedDataOffers) { + public void updateDataOffers(DSLContext dsl, String connectorEndpoint, Collection fetchedDataOffers, ConnectorChangeTracker changes) { var patch = dataOfferPatchBuilder.buildDataOfferPatch(dsl, connectorEndpoint, fetchedDataOffers); + changes.setNumOffersAdded(patch.getDataOffersToInsert().size()); + changes.setNumOffersUpdated(patch.getDataOffersToUpdate().size()); + changes.setNumOffersDeleted(patch.getDataOffersToDelete().size()); dataOfferPatchApplier.writeDataOfferPatch(dsl, patch); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index b3aeff9de..b29c071e4 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -53,6 +53,7 @@ void testQueryConnectors() { var connector = result.getConnectors().get(0); assertThat(connector.getEndpoint()).isEqualTo("https://example.com/ids/data"); + assertThat(connector.getId()).isEqualTo("https://example.com"); assertThat(connector.getOnlineStatus()).isEqualTo(OFFLINE); } } From 5c4fb3d359b08428d79ef12934a46ab8f8ee3d61 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 1 Jun 2023 10:27:35 +0200 Subject: [PATCH 033/295] feat: prevent refresh queue overflows (#91) * feat: prioritise never refreshed connectors * feat: getQueuedConnectorEndpoints and skip tasks that are already queued * feat: skip tasks that are already queued --- .../services/queue/ConnectorQueue.java | 7 ++++++- .../services/queue/ThreadPool.java | 18 ++++++++++++++++-- .../services/queue/ThreadPoolTask.java | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java index 28944efe6..f8ba03b1c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java @@ -17,6 +17,7 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; import lombok.RequiredArgsConstructor; +import java.util.ArrayList; import java.util.Collection; @RequiredArgsConstructor @@ -31,8 +32,12 @@ public class ConnectorQueue { * @param priority priority from {@link ConnectorRefreshPriority} */ public void addAll(Collection endpoints, int priority) { + var queuedConnectorEndpoints = threadPool.getQueuedConnectorEndpoints(); + endpoints = new ArrayList<>(endpoints); + endpoints.removeIf(queuedConnectorEndpoints::contains); + for (String endpoint : endpoints) { - threadPool.execute(priority, () -> connectorUpdater.updateConnector(endpoint)); + threadPool.execute(priority, () -> connectorUpdater.updateConnector(endpoint), endpoint); } } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java index 5119a3ee8..1cc4b3123 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java @@ -17,8 +17,12 @@ import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import org.eclipse.edc.spi.system.configuration.Config; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; public class ThreadPool { private final PriorityBlockingQueue queue; @@ -33,7 +37,17 @@ public ThreadPool(Config config) { threadPoolExecutor.prestartAllCoreThreads(); } - public void execute(int priority, Runnable runnable) { - queue.add(new ThreadPoolTask(priority, runnable)); + public void execute(int priority, Runnable runnable, String endpoint) { + queue.add(new ThreadPoolTask(priority, runnable, endpoint)); + } + + public Set getQueuedConnectorEndpoints() { + var queuedRunnables = new ArrayList<>(queue); + + return queuedRunnables.stream().filter(ThreadPoolTask.class::isInstance) + .map(ThreadPoolTask.class::cast) + .map(ThreadPoolTask::getConnectorEndpoint) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java index f7bfcd619..60c6804fc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java @@ -24,6 +24,7 @@ public class ThreadPoolTask implements Comparable, Runnable { private final int priority; private final Runnable task; + private final String connectorEndpoint; @Override public int compareTo(@NotNull ThreadPoolTask threadPoolTask) { From a10fc19db94eaba96d33cb71d5d81a92e0cba6fb Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 1 Jun 2023 14:17:59 +0200 Subject: [PATCH 034/295] test: DataOfferWriter (and patching mechanisms behind it) (#96) * test: DataOfferWriter (and patching mechanisms behind it) * chore: checkstyle * refactor: minor remarks --------- Co-authored-by: Tim Berthold --- .../db/migration/{V1__MS8.sql => V1__MS9.sql} | 303 ++---------------- .../ext/brokerserver/dao/AssetProperty.java | 2 +- .../dao/queries/DataOfferQueries.java | 2 +- .../dao/queries/utils/JsonbUtils.java | 36 +++ .../services/queue/ThreadPool.java | 12 +- .../offers/ContractOfferRecordUpdater.java | 20 +- .../offers/DataOfferPatchBuilder.java | 31 +- .../offers/DataOfferRecordUpdater.java | 19 +- .../services/refreshing/offers/DiffUtils.java | 5 +- .../offers/DataOfferWriterTest.java | 168 ++++++++++ .../offers/DataOfferWriterTestDataHelper.java | 124 +++++++ .../offers/DataOfferWriterTestDataModels.java | 62 ++++ .../offers/DataOfferWriterTestDydi.java | 41 +++ .../DataOfferWriterTestResultHelper.java | 56 ++++ .../refreshing/offers/DiffUtilsTest.java | 41 +++ 15 files changed, 625 insertions(+), 297 deletions(-) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V1__MS8.sql => V1__MS9.sql} (52%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS9.sql similarity index 52% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS9.sql index 9c5ff65f3..16e6c9ec4 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS9.sql @@ -16,7 +16,8 @@ -- table: edc_asset CREATE TABLE IF NOT EXISTS edc_asset ( - asset_id VARCHAR NOT NULL, + asset_id VARCHAR NOT NULL, + created_at BIGINT NOT NULL, PRIMARY KEY (asset_id) ); @@ -33,10 +34,11 @@ COMMENT ON COLUMN edc_asset_dataaddress.properties IS 'DataAddress properties se -- table: edc_asset_property CREATE TABLE IF NOT EXISTS edc_asset_property ( - asset_id_fk VARCHAR NOT NULL, - property_name VARCHAR NOT NULL, - property_value VARCHAR NOT NULL, - property_type VARCHAR NOT NULL, + asset_id_fk VARCHAR NOT NULL, + property_name VARCHAR NOT NULL, + property_value VARCHAR NOT NULL, + property_type VARCHAR NOT NULL, + property_is_private BOOLEAN NOT NULL, PRIMARY KEY (asset_id_fk, property_name), FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE ); @@ -47,33 +49,12 @@ COMMENT ON COLUMN edc_asset_property.property_value IS 'Asset property value'; COMMENT ON COLUMN edc_asset_property.property_type IS 'Asset property class name'; +COMMENT ON COLUMN edc_asset_property.property_is_private IS + 'Asset property private flag'; CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value ON edc_asset_property (property_name, property_value); --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- - -ALTER TABLE edc_asset - ADD created_at BIGINT; - -UPDATE edc_asset -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; - -ALTER TABLE edc_asset - ALTER COLUMN created_at SET NOT NULL; - -- -- Copyright (c) 2022 Daimler TSS GmbH -- @@ -92,68 +73,14 @@ ALTER TABLE edc_asset -- only intended for and tested with H2 and Postgres! CREATE TABLE IF NOT EXISTS edc_contract_definitions ( + created_at BIGINT NOT NULL, contract_definition_id VARCHAR NOT NULL, access_policy_id VARCHAR NOT NULL, contract_policy_id VARCHAR NOT NULL, - selector_expression JSON NOT NULL, + assets_selector JSON NOT NULL, PRIMARY KEY (contract_definition_id) ); --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- -ALTER TABLE edc_contract_definitions - ADD created_at BIGINT; - -UPDATE edc_contract_definitions -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; - -ALTER TABLE edc_contract_definitions - ALTER COLUMN created_at SET NOT NULL; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- - -ALTER TABLE edc_contract_definitions - ADD validity BIGINT; --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- -UPDATE edc_contract_definitions -SET validity=60 * 60 * 24 * 365 -WHERE validity IS NULL; - -- Statements are designed for and tested with Postgres only! CREATE TABLE IF NOT EXISTS edc_lease @@ -196,11 +123,13 @@ CREATE TABLE IF NOT EXISTS edc_contract_negotiation id VARCHAR NOT NULL CONSTRAINT contract_negotiation_pk PRIMARY KEY, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, correlation_id VARCHAR, counterparty_id VARCHAR NOT NULL, counterparty_address VARCHAR NOT NULL, - protocol VARCHAR DEFAULT 'ids-multipart'::CHARACTER VARYING NOT NULL, - type INTEGER DEFAULT 0 NOT NULL, + protocol VARCHAR NOT NULL, + type VARCHAR NOT NULL, state INTEGER DEFAULT 0 NOT NULL, state_count INTEGER DEFAULT 0, state_timestamp BIGINT, @@ -209,6 +138,7 @@ CREATE TABLE IF NOT EXISTS edc_contract_negotiation CONSTRAINT contract_negotiation_contract_agreement_id_fk REFERENCES edc_contract_agreement, contract_offers JSON, + callback_addresses JSON, trace_context JSON, lease_id VARCHAR CONSTRAINT contract_negotiation_lease_lease_id_fk @@ -233,88 +163,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS contract_negotiation_id_uindex CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex ON edc_contract_agreement (agr_id); --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- -ALTER TABLE edc_contract_negotiation - ADD created_at BIGINT, - ADD updated_at BIGINT; - -UPDATE edc_contract_negotiation -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; -UPDATE edc_contract_negotiation -SET updated_at=created_at; - -ALTER TABLE edc_contract_negotiation - ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE edc_contract_negotiation - ALTER COLUMN updated_at SET NOT NULL; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- - -UPDATE edc_contract_negotiation -SET contract_offers = co.contract_offers_edited -FROM (SELECT cn.id, - jsonb_agg( - jsonb_set( - jsonb_set( - elems, - '{contractStart}', - to_json(to_char(to_timestamp(created_at / 1000) AT TIME ZONE 'UTC', - 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb - ), - '{contractEnd}', - to_json(to_char(to_timestamp((created_at / 1000) + 60 * 60 * 24 * 365) AT TIME ZONE 'UTC', - 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb - ) - )::json as contract_offers_edited - FROM edc_contract_negotiation cn, - jsonb_array_elements(cn.contract_offers::jsonb) elems - GROUP BY cn.id) co -WHERE edc_contract_negotiation.id = co.id; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - initial API and implementation for DataplaneInstances --- --- -CREATE TABLE IF NOT EXISTS edc_data_plane_instance -( - id VARCHAR NOT NULL, - data JSON NOT NULL, - PRIMARY KEY (id) -); - -- -- Copyright (c) 2022 ZF Friedrichshafen AG -- @@ -334,6 +182,7 @@ CREATE TABLE IF NOT EXISTS edc_data_plane_instance CREATE TABLE IF NOT EXISTS edc_policydefinitions ( policy_id VARCHAR NOT NULL, + created_at BIGINT NOT NULL, permissions JSON, prohibitions JSON, duties JSON, @@ -355,28 +204,6 @@ COMMENT ON COLUMN edc_policydefinitions.policy_type IS 'Java PolicyType serializ CREATE UNIQUE INDEX IF NOT EXISTS edc_policydefinitions_id_uindex ON edc_policydefinitions (policy_id); --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- -ALTER TABLE edc_policydefinitions - ADD created_at BIGINT; - -UPDATE edc_policydefinitions -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; - -ALTER TABLE edc_policydefinitions - ALTER COLUMN created_at SET NOT NULL; - -- Statements are designed for and tested with Postgres only! CREATE TABLE IF NOT EXISTS edc_lease @@ -398,18 +225,21 @@ CREATE TABLE IF NOT EXISTS edc_transfer_process transferprocess_id VARCHAR NOT NULL CONSTRAINT transfer_process_pk PRIMARY KEY, - type VARCHAR NOT NULL, - state INTEGER NOT NULL, - state_count INTEGER DEFAULT 0 NOT NULL, - state_time_stamp BIGINT, - created_time_stamp BIGINT, - trace_context JSON, - error_detail VARCHAR, - resource_manifest JSON, - provisioned_resource_set JSON, - content_data_address JSON, - deprovisioned_resources JSON, - lease_id VARCHAR + type VARCHAR NOT NULL, + state INTEGER NOT NULL, + state_count INTEGER DEFAULT 0 NOT NULL, + state_time_stamp BIGINT, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + trace_context JSON, + error_detail VARCHAR, + resource_manifest JSON, + provisioned_resource_set JSON, + content_data_address JSON, + deprovisioned_resources JSON, + private_properties JSON, + callback_addresses JSON, + lease_id VARCHAR CONSTRAINT transfer_process_lease_lease_id_fk REFERENCES edc_lease ON DELETE SET NULL @@ -443,7 +273,6 @@ CREATE TABLE IF NOT EXISTS edc_data_request data_destination JSON NOT NULL, managed_resources BOOLEAN DEFAULT TRUE, properties JSON, - transfer_type JSON, transfer_process_id VARCHAR NOT NULL CONSTRAINT data_request_transfer_process_id_fk REFERENCES edc_transfer_process @@ -454,76 +283,8 @@ COMMENT ON COLUMN edc_data_request.data_destination IS 'DataAddress serialized a COMMENT ON COLUMN edc_data_request.properties IS 'java Map serialized as JSON'; -COMMENT ON COLUMN edc_data_request.transfer_type IS 'TransferType serialized as JSON'; - - CREATE UNIQUE INDEX IF NOT EXISTS data_request_id_uindex ON edc_data_request (datarequest_id); CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex ON edc_lease (lease_id); - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- - -ALTER TABLE edc_transfer_process - RENAME COLUMN created_time_stamp TO created_at; - -UPDATE edc_transfer_process -SET created_at = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 -WHERE created_at = NULL; - -ALTER TABLE edc_transfer_process - ADD updated_at BIGINT; - -UPDATE edc_transfer_process -SET updated_at=created_at; - -ALTER TABLE edc_transfer_process - ALTER COLUMN updated_at SET NOT NULL; -ALTER TABLE edc_transfer_process - ALTER COLUMN created_at SET NOT NULL; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- -ALTER TABLE edc_transfer_process - ADD transferprocess_properties JSON; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- -UPDATE edc_transfer_process -SET transferprocess_properties = '{}'::json -WHERE transferprocess_properties IS NULL; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java index a5fda457a..e66740284 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java @@ -21,7 +21,7 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class AssetProperty { public static final String ASSET_ID = "asset:prop:id"; - public static final String TITLE = "asset:prop:name"; + public static final String ASSET_NAME = "asset:prop:name"; public static final String DESCRIPTION = "asset:prop:description"; public static final String KEYWORDS = "asset:prop:keywords"; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java index 8cd86dc32..7b5274f99 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java @@ -47,7 +47,7 @@ public List forCatalogPage(DSLContext dsl, String searchQuery, C // Asset Properties from JSON to be used in sorting / filtering var assetId = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.ASSET_ID); - var assetTitle = DSL.coalesce(JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.TITLE), assetId); + var assetTitle = DSL.coalesce(JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.ASSET_NAME), assetId); var assetDescription = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.DESCRIPTION); var assetKeywords = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.KEYWORDS); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java new file mode 100644 index 000000000..42dff4864 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.queries.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.jooq.JSONB; + +/** + * Utilities for dealing with {@link org.jooq.JSONB} fields. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonbUtils { + + /** + * Returns the data of the given {@link JSONB} or null. + * + * @param jsonb {@link org.jooq.JSON} + * @return data or null + */ + public static String getDataOrNull(JSONB jsonb) { + return jsonb == null ? null : jsonb.data(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java index 1cc4b3123..541c21e34 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class ThreadPool { @@ -29,12 +30,11 @@ public class ThreadPool { public ThreadPool(Config config) { this.queue = new PriorityBlockingQueue<>(); - var threadPoolExecutor = new ThreadPoolExecutor(1, - config.getInteger(BrokerServerExtension.NUM_THREADS, 1), - 60, - java.util.concurrent.TimeUnit.SECONDS, - queue); - threadPoolExecutor.prestartAllCoreThreads(); + int numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); + if (numThreads > 0) { + var threadPoolExecutor = new ThreadPoolExecutor(1, numThreads, 60, TimeUnit.SECONDS, queue); + threadPoolExecutor.prestartAllCoreThreads(); + } } public void execute(int priority, Runnable runnable, String endpoint) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java index f8b53803a..fa6a4cebc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +import de.sovity.edc.ext.brokerserver.dao.queries.utils.JsonbUtils; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; @@ -41,10 +42,11 @@ public class ContractOfferRecordUpdater { public DataOfferContractOfferRecord newContractOffer(DataOfferRecord dataOffer, FetchedDataOfferContractOffer fetchedContractOffer) { var contractOffer = new DataOfferContractOfferRecord(); contractOffer.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); + contractOffer.setContractOfferId(fetchedContractOffer.getContractOfferId()); contractOffer.setAssetId(dataOffer.getAssetId()); contractOffer.setCreatedAt(OffsetDateTime.now()); updateContractOffer(contractOffer, fetchedContractOffer); - return null; + return contractOffer; } /** @@ -55,11 +57,19 @@ public DataOfferContractOfferRecord newContractOffer(DataOfferRecord dataOffer, * @return if anything was changed */ public boolean updateContractOffer(DataOfferContractOfferRecord contractOffer, FetchedDataOfferContractOffer fetchedContractOffer) { - if (!Objects.equals(contractOffer.getPolicy().data(), fetchedContractOffer.getPolicyJson())) { - contractOffer.setPolicy(JSONB.jsonb(fetchedContractOffer.getPolicyJson())); + var existingPolicy = JsonbUtils.getDataOrNull(contractOffer.getPolicy()); + var fetchedPolicy = fetchedContractOffer.getPolicyJson(); + var changed = false; + + if (!Objects.equals(existingPolicy, fetchedPolicy)) { + contractOffer.setPolicy(JSONB.jsonb(fetchedPolicy)); + changed = true; + } + + if (changed) { contractOffer.setUpdatedAt(OffsetDateTime.now()); - return true; } - return false; + + return changed; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java index 8e56e37ca..96da9969e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import static java.util.stream.Collectors.groupingBy; @@ -72,24 +73,35 @@ public DataOfferPatch buildDataOfferPatch( var existing = match.existing(); var fetched = match.fetched(); - if (dataOfferRecordUpdater.updateDataOffer(existing, fetched)) { + // Update Contract Offers + var contractOffers = contractOffersByAssetId.getOrDefault(existing.getAssetId(), List.of()); + var changed = patchContractOffers(patch, existing, contractOffers, fetched.getContractOffers()); + + // Update Data Offer (and update updatedAt if contractOffers changed) + changed = dataOfferRecordUpdater.updateDataOffer(existing, fetched, changed); + + if (changed) { patch.updateDataOffer(existing); } - var contractOffers = contractOffersByAssetId.getOrDefault(existing.getAssetId(), List.of()); - patchContractOffers(patch, existing, contractOffers, fetched.getContractOffers()); }); - diff.removed().forEach(patch::deleteDataOffer); + diff.removed().forEach(dataOffer -> { + patch.deleteDataOffer(dataOffer); + var contractOffers = contractOffersByAssetId.getOrDefault(dataOffer.getAssetId(), List.of()); + contractOffers.forEach(patch::deleteContractOffer); + }); return patch; } - private void patchContractOffers( + private boolean patchContractOffers( DataOfferPatch patch, DataOfferRecord dataOffer, Collection contractOffers, Collection fetchedContractOffers ) { + var hasUpdates = new AtomicBoolean(false); + var diff = DiffUtils.compareLists( contractOffers, DataOfferContractOfferRecord::getContractOfferId, @@ -100,6 +112,7 @@ private void patchContractOffers( diff.added().forEach(fetched -> { var newRecord = contractOfferRecordUpdater.newContractOffer(dataOffer, fetched); patch.insertContractOffer(newRecord); + hasUpdates.set(true); }); diff.updated().forEach(match -> { @@ -108,9 +121,15 @@ private void patchContractOffers( if (contractOfferRecordUpdater.updateContractOffer(existing, fetched)) { patch.updateContractOffer(existing); + hasUpdates.set(true); } }); - diff.removed().forEach(patch::deleteContractOffer); + diff.removed().forEach(existing -> { + patch.deleteContractOffer(existing); + hasUpdates.set(true); + }); + + return hasUpdates.get(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java index 338006ed7..73a33c214 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +import de.sovity.edc.ext.brokerserver.dao.queries.utils.JsonbUtils; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; import lombok.RequiredArgsConstructor; @@ -41,7 +42,7 @@ public DataOfferRecord newDataOffer(String connectorEndpoint, FetchedDataOffer f dataOffer.setConnectorEndpoint(connectorEndpoint); dataOffer.setAssetId(fetchedDataOffer.getAssetId()); dataOffer.setCreatedAt(OffsetDateTime.now()); - updateDataOffer(dataOffer, fetchedDataOffer); + updateDataOffer(dataOffer, fetchedDataOffer, true); return dataOffer; } @@ -51,15 +52,21 @@ public DataOfferRecord newDataOffer(String connectorEndpoint, FetchedDataOffer f * * @param dataOffer existing row * @param fetchedDataOffer changes to be incorporated + * @param changed whether the data offer should be marked as updated simply because the contract offers changed * @return whether any fields were updated */ - public boolean updateDataOffer(DataOfferRecord dataOffer, FetchedDataOffer fetchedDataOffer) { - String existingAssetProps = dataOffer.getAssetProperties().data(); - String fetchedAssetProps = fetchedDataOffer.getAssetPropertiesJson(); + public boolean updateDataOffer(DataOfferRecord dataOffer, FetchedDataOffer fetchedDataOffer, boolean changed) { + String existingAssetProps = JsonbUtils.getDataOrNull(dataOffer.getAssetProperties()); + var fetchedAssetProps = fetchedDataOffer.getAssetPropertiesJson(); if (!Objects.equals(fetchedAssetProps, existingAssetProps)) { dataOffer.setAssetProperties(JSONB.jsonb(fetchedAssetProps)); - return true; + changed = true; } - return false; + + if (changed) { + dataOffer.setUpdatedAt(OffsetDateTime.now()); + } + + return changed; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java index 5b9bb7537..413ba77a2 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java @@ -50,7 +50,7 @@ public static DiffResult compareLists( var keys = new HashSet<>(existingByKey.keySet()); keys.addAll(fetchedByKey.keySet()); - var result = new DiffResult(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + var result = new DiffResult(); keys.forEach(key -> { var existingItem = existingByKey.get(key); @@ -78,6 +78,9 @@ public static DiffResult compareLists( * @param fetched item type */ record DiffResult(List added, List> updated, List removed) { + DiffResult() { + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + } } /** diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java new file mode 100644 index 000000000..30e7ea48f --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Do; +import org.assertj.core.data.TemporalUnitLessThanOffset; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class DataOfferWriterTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( + BrokerServerExtension.KNOWN_CONNECTORS, "https://example.com/ids/data", + BrokerServerExtension.NUM_THREADS, "0" + ))); + } + + @Test + void testDataOfferWriter_allSortsOfUpdates() { + TEST_DATABASE.testTransaction(dsl -> { + var testDydi = new DataOfferWriterTestDydi(); + var testData = new DataOfferWriterTestDataHelper(); + var changes = new ConnectorChangeTracker(); + var dataOfferWriter = testDydi.getDataOfferWriter(); + + // arrange + var unchanged = Do.forName("unchanged"); + testData.existing(unchanged); + testData.fetched(unchanged); + + var fieldChangedExisting = Do.forName("fieldChanged"); + var fieldChangedFetched = fieldChangedExisting.withAssetName("changed"); + testData.existing(fieldChangedExisting); + testData.fetched(fieldChangedFetched); + + var added = Do.forName("added"); + testData.fetched(added); + + var removed = Do.forName("removed"); + testData.existing(removed); + + var changedCoExisting = Do.forName("contractOffer"); + var changedCoFetched = changedCoExisting.withContractOffers(List.of( + changedCoExisting.getContractOffers().get(0).withPolicyValue("changed") + )); + testData.existing(changedCoExisting); + testData.fetched(changedCoFetched); + + var addedCoExisting = Do.forName("contractOfferAdded"); + var addedCoFetched = addedCoExisting.withContractOffer(new Co("added co", "added co")); + testData.existing(addedCoExisting); + testData.fetched(addedCoFetched); + + var removedCoExisting = Do.forName("contractOfferRemoved").withContractOffer(new Co("removed co", "removed co")); + var removedCoFetched = Do.forName("contractOfferRemoved"); + testData.existing(removedCoExisting); + testData.fetched(removedCoFetched); + + // act + dsl.transaction(it -> testData.initialize(it.dsl())); + dsl.transaction(it -> dataOfferWriter.updateDataOffers( + it.dsl(), + testData.connectorEndpoint, + testData.fetchedDataOffers, + changes + )); + var actual = dsl.transactionResult(it -> new DataOfferWriterTestResultHelper(it.dsl())); + + // assert + assertThat(actual.numDataOffers()).isEqualTo(6); + assertThat(changes.getNumOffersAdded()).isEqualTo(1); + assertThat(changes.getNumOffersUpdated()).isEqualTo(4); + assertThat(changes.getNumOffersDeleted()).isEqualTo(1); + + var now = OffsetDateTime.now(); + var minuteAccuracy = new TemporalUnitLessThanOffset(1, ChronoUnit.MINUTES); + var addedActual = actual.getDataOffer(added.getAssetId()); + assertAssetPropertiesEqual(testData, addedActual, added); + assertThat(addedActual.getCreatedAt()).isCloseTo(now, minuteAccuracy); + assertThat(addedActual.getUpdatedAt()).isCloseTo(now, minuteAccuracy); + assertThat(actual.numContractOffers(added.getAssetId())).isEqualTo(1); + assertPolicyEquals(actual, testData, added, added.getContractOffers().get(0)); + + var unchangedActual = actual.getDataOffer(unchanged.getAssetId()); + assertThat(unchangedActual.getUpdatedAt()).isEqualTo(testData.old); + assertThat(unchangedActual.getCreatedAt()).isEqualTo(testData.old); + + var fieldChangedActual = actual.getDataOffer(fieldChangedExisting.getAssetId()); + assertAssetPropertiesEqual(testData, fieldChangedActual, fieldChangedFetched); + assertThat(fieldChangedActual.getCreatedAt()).isEqualTo(testData.old); + assertThat(fieldChangedActual.getUpdatedAt()).isCloseTo(now, minuteAccuracy); + + var removedActual = actual.getDataOffer(removed.getAssetId()); + assertThat(removedActual).isNull(); + + var changedCoActual = actual.getDataOffer(changedCoExisting.getAssetId()); + assertThat(changedCoActual.getCreatedAt()).isEqualTo(testData.old); + assertThat(changedCoActual.getUpdatedAt()).isCloseTo(now, minuteAccuracy); + assertThat(actual.numContractOffers(changedCoExisting.getAssetId())).isEqualTo(1); + assertPolicyEquals(actual, testData, changedCoFetched, changedCoFetched.getContractOffers().get(0)); + + var addedCoActual = actual.getDataOffer(addedCoExisting.getAssetId()); + assertThat(addedCoActual.getCreatedAt()).isEqualTo(testData.old); + assertThat(addedCoActual.getUpdatedAt()).isCloseTo(now, minuteAccuracy); + assertThat(actual.numContractOffers(addedCoActual.getAssetId())).isEqualTo(2); + + var removedCoActual = actual.getDataOffer(removedCoExisting.getAssetId()); + assertThat(removedCoActual.getCreatedAt()).isEqualTo(testData.old); + assertThat(removedCoActual.getUpdatedAt()).isCloseTo(now, minuteAccuracy); + assertThat(actual.numContractOffers(removedCoActual.getAssetId())).isEqualTo(1); + }); + } + + private void assertAssetPropertiesEqual(DataOfferWriterTestDataHelper testData, DataOfferRecord actual, Do expected) { + var actualAssetJson = actual.getAssetProperties().data(); + var expectedAssetJson = testData.dummyAssetJson(expected); + assertThat(actualAssetJson).isEqualTo(expectedAssetJson); + } + + private void assertPolicyEquals( + DataOfferWriterTestResultHelper actual, + DataOfferWriterTestDataHelper scenario, + Do expectedDo, + Co expectedCo + ) { + var actualContractOffer = actual.getContractOffer(expectedDo.getAssetId(), expectedCo.getId()); + var actualPolicy = actualContractOffer.getPolicy().data(); + var expectedPolicy = scenario.dummyPolicyJson(expectedCo.getPolicyValue()); + assertThat(actualPolicy).isEqualTo(expectedPolicy); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java new file mode 100644 index 000000000..b20bfa2ac --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.JSONB; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; +import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Do; + +class DataOfferWriterTestDataHelper { + String connectorEndpoint = "https://example.com/ids/data"; + OffsetDateTime old = OffsetDateTime.now().withNano(0).withSecond(0).withMinute(0).withHour(0).minusDays(100); + List existingContractOffers = new ArrayList<>(); + List existingDataOffers = new ArrayList<>(); + List fetchedDataOffers = new ArrayList<>(); + + + /** + * Adds fetched data offer + * + * @param dataOffer fetched data offer + */ + public void fetched(Do dataOffer) { + Validate.notEmpty(dataOffer.getContractOffers()); + fetchedDataOffers.add(dummyFetchedDataOffer(dataOffer)); + } + + + /** + * Adds data offer directly to DB. + * + * @param dataOffer data offer + */ + public void existing(Do dataOffer) { + Validate.notEmpty(dataOffer.getContractOffers()); + existingDataOffers.add(dummyDataOffer(dataOffer)); + dataOffer.getContractOffers().stream() + .map(contractOffer -> dummyContractOffer(dataOffer, contractOffer)) + .forEach(existingContractOffers::add); + } + + public void initialize(DSLContext dsl) { + dsl.batchInsert(existingDataOffers).execute(); + dsl.batchInsert(existingContractOffers).execute(); + } + + private DataOfferContractOfferRecord dummyContractOffer(Do dataOffer, Co contractOffer) { + return new DataOfferContractOfferRecord( + contractOffer.getId(), + connectorEndpoint, + dataOffer.getAssetId(), + JSONB.valueOf(dummyPolicyJson(contractOffer.getPolicyValue())), + old, + old + ); + } + + private DataOfferRecord dummyDataOffer(Do dataOffer) { + return new DataOfferRecord( + connectorEndpoint, + dataOffer.getAssetId(), + JSONB.valueOf(dummyAssetJson(dataOffer)), + old, + old + ); + } + + private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { + var fetchedDataOffer = new FetchedDataOffer(); + fetchedDataOffer.setAssetId(dataOffer.getAssetId()); + fetchedDataOffer.setAssetPropertiesJson(dummyAssetJson(dataOffer)); + + var contractOffersMapped = dataOffer.getContractOffers().stream().map(this::dummyFetchedContractOffer).collect(Collectors.toList()); + fetchedDataOffer.setContractOffers(contractOffersMapped); + + return fetchedDataOffer; + } + + public String dummyAssetJson(Do dataOffer) { + return "{\"%s\": \"%s\", \"%s\": \"%s\"}".formatted( + AssetProperty.ASSET_ID, dataOffer.getAssetId(), + AssetProperty.ASSET_NAME, dataOffer.getAssetName() + ); + } + + public String dummyPolicyJson(String policyValue) { + return "{\"%s\": \"%s\"}".formatted( + "SomePolicyField", policyValue + ); + } + + @NotNull + private FetchedDataOfferContractOffer dummyFetchedContractOffer(Co it) { + var contractOffer = new FetchedDataOfferContractOffer(); + contractOffer.setContractOfferId(it.getId()); + contractOffer.setPolicyJson(dummyPolicyJson(it.getPolicyValue())); + return contractOffer; + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java new file mode 100644 index 000000000..ca7343a94 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import lombok.Value; +import lombok.With; + +import java.util.ArrayList; +import java.util.List; + +class DataOfferWriterTestDataModels { + /** + * Dummy Data Offer + */ + @Value + static class Do { + @With + String assetId; + @With + String assetName; + @With + List contractOffers; + + public Do withContractOffer(Co co) { + var list = new ArrayList<>(contractOffers); + list.add(co); + return this.withContractOffers(list); + } + + public static Do forName(String name) { + return new Do(name, name + " Name", List.of(new Co(name + " CO", name + " Policy"))); + } + } + + /** + * Dummy Contract Offer + */ + @Value + static class Co { + @With + String id; + @With + String policyValue; + } + + public static Co forName(String name) { + return new Co(name + " CO", name + " Policy"); + } + +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java new file mode 100644 index 000000000..9bab06b9a --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; +import lombok.Value; +import org.eclipse.edc.spi.system.configuration.Config; + +import static org.mockito.Mockito.mock; + +@Value +class DataOfferWriterTestDydi { + Config config = mock(Config.class); + BrokerServerSettings brokerServerSettings = new BrokerServerSettings(config); + DataOfferQueries dataOfferQueries = new DataOfferQueries(brokerServerSettings); + DataOfferContractOfferQueries dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); + ContractOfferRecordUpdater contractOfferRecordUpdater = new ContractOfferRecordUpdater(); + DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater(); + DataOfferPatchBuilder dataOfferPatchBuilder = new DataOfferPatchBuilder( + dataOfferContractOfferQueries, + dataOfferQueries, + dataOfferRecordUpdater, + contractOfferRecordUpdater + ); + DataOfferPatchApplier dataOfferPatchApplier = new DataOfferPatchApplier(); + DataOfferWriter dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java new file mode 100644 index 000000000..acdefc766 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; + +class DataOfferWriterTestResultHelper { + private final @NotNull Map dataOffers; + private final @NotNull Map> contractOffers; + + DataOfferWriterTestResultHelper(DSLContext dsl) { + this.dataOffers = dsl.selectFrom(Tables.DATA_OFFER).fetchMap(Tables.DATA_OFFER.ASSET_ID); + this.contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().collect(groupingBy( + DataOfferContractOfferRecord::getAssetId, + Collectors.toMap(DataOfferContractOfferRecord::getContractOfferId, Function.identity()) + )); + } + + public DataOfferRecord getDataOffer(String assetId) { + return dataOffers.get(assetId); + } + + public int numDataOffers() { + return dataOffers.size(); + } + + public int numContractOffers(String assetId) { + return contractOffers.getOrDefault(assetId, Map.of()).size(); + } + + public DataOfferContractOfferRecord getContractOffer(String assetId, String contractOfferId) { + return contractOffers.getOrDefault(assetId, Map.of()).get(contractOfferId); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java new file mode 100644 index 000000000..7a35e66db --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DiffUtils.DiffResultMatch; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +class DiffUtilsTest { + + @Test + void testCompareLists() { + // arrange + List existing = List.of(1, 2); + List fetched = List.of("1", "3"); + + // act + var actual = DiffUtils.compareLists(existing, Function.identity(), fetched, Integer::parseInt); + + // assert + assertThat(actual.added()).containsExactly("3"); + assertThat(actual.updated()).containsExactly(new DiffResultMatch<>(1, "1")); + assertThat(actual.removed()).containsExactly(2); + } +} From b1f8042464557a22162a32ab915d92882fa91e42 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:28:46 +0200 Subject: [PATCH 035/295] feat: log connector online status changes (#95) --- .../services/logging/BrokerEventLogger.java | 18 ++++++++++++++++++ .../logging/ConnectorChangeTracker.java | 2 +- .../ConnectorUpdateFailureWriter.java | 12 +++++++----- .../ConnectorUpdateSuccessWriter.java | 15 +++++++++++---- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index 77cb0d999..6a3d9f2f9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -17,6 +17,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.BrokerEventLogRecord; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -44,6 +45,23 @@ public void logConnectorUpdateFailure(DSLContext dsl, String connectorEndpoint, logEntry.insert(); } + public void logConnectorUpdateStatusChange(DSLContext dsl, String connectorEndpoint, ConnectorOnlineStatus status) { + var logEntry = connectorUpdateEntry(dsl, connectorEndpoint); + switch (status) { + case ONLINE: + logEntry.setUserMessage("Connector is online: " + connectorEndpoint); + logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_ONLINE); + break; + case OFFLINE: + logEntry.setUserMessage("Connector is offline: " + connectorEndpoint); + logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE); + break; + default: + throw new IllegalArgumentException("Unknown status: " + status + " for connector: " + connectorEndpoint); + } + logEntry.insert(); + } + private BrokerEventLogRecord connectorUpdateEntry(DSLContext dsl, String connectorEndpoint) { var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java index 61328ca24..9f69fef8c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java @@ -44,7 +44,7 @@ public void addSelfDescriptionChange(String name) { } public boolean isEmpty() { - return selfDescriptionChanges.isEmpty(); + return selfDescriptionChanges.isEmpty() && numOffersAdded == 0 && numOffersDeleted == 0 && numOffersUpdated == 0; } @Override diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java index eca142c26..caa70f00c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java @@ -29,13 +29,15 @@ public class ConnectorUpdateFailureWriter { private final BrokerEventLogger brokerEventLogger; public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Throwable e) { - // Update Connector - connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + // Log Status Change and set status to offline if necessary + if (connector.getOnlineStatus() == ConnectorOnlineStatus.ONLINE) { + brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.OFFLINE); + brokerEventLogger.logConnectorUpdateFailure(dsl, connector.getEndpoint(), getFailureMessage(e)); + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + } + connector.setLastRefreshAttemptAt(OffsetDateTime.now()); connector.update(); - - // Log Event - brokerEventLogger.logConnectorUpdateFailure(dsl, connector.getEndpoint(), getFailureMessage(e)); } public BrokerEventErrorMessage getFailureMessage(Throwable e) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 30d88d543..7667df511 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -38,18 +38,25 @@ public void handleConnectorOnline( ) { OffsetDateTime now = OffsetDateTime.now(); + // Log Status Change and set status to online if necessary + if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE) { + brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.ONLINE); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + } + // Track changes for final log message ConnectorChangeTracker changes = new ConnectorChangeTracker(); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setLastSuccessfulRefreshAt(now); connector.setLastRefreshAttemptAt(now); connector.update(); + // Log Event if changes are present + if (!changes.isEmpty()) { + brokerEventLogger.logConnectorUpdateSuccess(dsl, connector.getEndpoint(), changes); + } + // Update data offers dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), dataOffers, changes); - - // Log Event - brokerEventLogger.logConnectorUpdateSuccess(dsl, connector.getEndpoint(), changes); } } From 91296b00f6efb721b1c3095b23db0248b32c0e09 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:41:23 +0200 Subject: [PATCH 036/295] refactor: minor static code analysis remarks (#97) * refactor: minor static code analysis remarks * refactor: minor static code analysis remarks --- .../edc/ext/brokerserver/services/BrokerServerSettings.java | 2 +- .../edc/ext/brokerserver/services/ConnectorCreator.java | 1 - .../brokerserver/services/KnownConnectorsInitializer.java | 4 ++-- .../ext/brokerserver/services/api/AssetPropertyParser.java | 4 ++-- .../ext/brokerserver/services/api/ConnectorApiService.java | 2 +- .../services/logging/BrokerEventErrorMessage.java | 2 +- .../services/logging/ConnectorChangeTracker.java | 2 +- .../services/queue/ConnectorRefreshPriority.java | 3 +++ .../edc/ext/brokerserver/services/queue/ThreadPoolTask.java | 2 ++ .../services/refreshing/ConnectorUpdateSuccessWriter.java | 4 ++-- .../java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java | 6 +++--- 11 files changed, 18 insertions(+), 14 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java index 2ebdf8062..3d45e0b64 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java @@ -34,7 +34,7 @@ public BrokerServerSettings(Config config) { } private Duration getDurationOrNull(@NonNull String configProperty) { - String durationAsString = config.getString(configProperty, ""); + var durationAsString = config.getString(configProperty, ""); if (StringUtils.isBlank(durationAsString)) { return null; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index a44cdd6d7..08efa524d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -47,7 +47,6 @@ public void addConnectors(DSLContext dsl, List connectorEndpoints) { @NotNull private ConnectorRecord newConnectorRow(String endpoint) { - var connector = new ConnectorRecord(); connector.setEndpoint(endpoint); connector.setConnectorId(UrlUtils.getEverythingBeforeThePath(endpoint)); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java index 8adad301a..117bdfbe8 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java @@ -32,13 +32,13 @@ public class KnownConnectorsInitializer { private final ConnectorCreator connectorCreator; public void addKnownConnectorsOnStartup(DSLContext dsl) { - List connectorEndpoints = getKnownConnectorsConfigValue(); + var connectorEndpoints = getKnownConnectorsConfigValue(); connectorCreator.addConnectors(dsl, connectorEndpoints); connectorQueue.addAll(connectorEndpoints, ConnectorRefreshPriority.ADDED_ON_STARTUP); } private List getKnownConnectorsConfigValue() { - String knownConnectorsString = config.getString(BrokerServerExtension.KNOWN_CONNECTORS, ""); + var knownConnectorsString = config.getString(BrokerServerExtension.KNOWN_CONNECTORS, ""); return Arrays.stream(knownConnectorsString.split(",")).map(String::trim).filter(StringUtils::isNotBlank).distinct().toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java index 51cb859df..2cb4fdf2c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java @@ -25,11 +25,11 @@ public class AssetPropertyParser { private final ObjectMapper objectMapper; - private final TypeReference> TYPE_TOKEN = new TypeReference<>() { + private final TypeReference> typeToken = new TypeReference<>() { }; @SneakyThrows public Map parsePropertiesFromJsonString(String assetPropertiesJson) { - return objectMapper.readValue(assetPropertiesJson, TYPE_TOKEN); + return objectMapper.readValue(assetPropertiesJson, typeToken); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 8a4ee63dc..19a4b7f2c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -51,7 +51,7 @@ private List buildConnectorListEntries(List, Runnable { private final int priority; private final Runnable task; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 7667df511..acca51f51 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -36,7 +36,7 @@ public void handleConnectorOnline( ConnectorRecord connector, Collection dataOffers ) { - OffsetDateTime now = OffsetDateTime.now(); + var now = OffsetDateTime.now(); // Log Status Change and set status to online if necessary if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE) { @@ -45,7 +45,7 @@ public void handleConnectorOnline( } // Track changes for final log message - ConnectorChangeTracker changes = new ConnectorChangeTracker(); + var changes = new ConnectorChangeTracker(); connector.setLastSuccessfulRefreshAt(now); connector.setLastRefreshAttemptAt(now); connector.update(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java index ea31570a5..d32425267 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java @@ -34,10 +34,10 @@ public class UrlUtils { */ public static String getEverythingBeforeThePath(String url) { var uri = URI.create(url); - String scheme = uri.getScheme(); // "http" - String authority = uri.getAuthority(); // "www.example.com" + var scheme = uri.getScheme(); // "http" + var authority = uri.getAuthority(); // "www.example.com" int port = uri.getPort(); // -1 (no port specified) - String everythingBeforePath = scheme + "://" + authority; + var everythingBeforePath = scheme + "://" + authority; if (port != -1) { everythingBeforePath += ":" + port; } From c06a3ea0ef9845f8ae9b097503761c3679de4012 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jun 2023 07:39:19 +0200 Subject: [PATCH 037/295] chore(deps): bump org.testcontainers:postgresql from 1.18.2 to 1.18.3 (#100) Bumps [org.testcontainers:postgresql](https://github.com/testcontainers/testcontainers-java) from 1.18.2 to 1.18.3. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.18.2...1.18.3) --- updated-dependencies: - dependency-name: org.testcontainers:postgresql dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 0653d4104..3a5d158d8 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -21,7 +21,7 @@ val postgresVersion: String by project buildscript { dependencies { - classpath("org.testcontainers:postgresql:1.18.2") + classpath("org.testcontainers:postgresql:1.18.3") } } From e0a467171e4e4be613ec97dd0f0d96716f63ae0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jun 2023 07:43:51 +0200 Subject: [PATCH 038/295] chore(deps): bump org.jooq:jooq from 3.16.4 to 3.18.4 (#62) Bumps org.jooq:jooq from 3.16.4 to 3.18.4. --- updated-dependencies: - dependency-name: org.jooq:jooq dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 3a5d158d8..60382c570 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -33,7 +33,7 @@ plugins { } dependencies { - api("org.jooq:jooq:3.16.4") + api("org.jooq:jooq:3.18.4") api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") jooqGenerator("org.postgresql:postgresql:42.6.0") From 09561533cd39a40955bd2898b1e9516dc8c9d318 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 2 Jun 2023 08:58:03 +0200 Subject: [PATCH 039/295] chore: try fix docker compose (#101) Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- .env | 4 ++-- connector/.env | 3 +++ docker-compose.yaml | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.env b/.env index cff794e01..019299700 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:3.2.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity5 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/connector/.env b/connector/.env index b4dca8621..bbbe7777d 100644 --- a/connector/.env +++ b/connector/.env @@ -5,7 +5,10 @@ # - Watch out for escaping issues as values will be surrounded by quotes, and dollar signs must be escaped. # Broker Server Related +EDC_BROKER_SERVER_KNOWN_CONNECTORS= EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D +EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * +EDC_BROKER_SERVER_NUM_THREADS=3 # Deployment Settings MY_EDC_FQDN=missing-env-MY_EDC_FQDN diff --git a/docker-compose.yaml b/docker-compose.yaml index 8a521c23e..d9477d1c3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,6 +23,12 @@ services: MY_EDC_MAINTAINER_URL: "https://sovity.de" MY_EDC_MAINTAINER_NAME: "sovity GmbH" + # Local Dev Broker Overrides + EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" + EDC_BROKER_SERVER_NUM_THREADS: "1" + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" + # Data Management API Key EDC_API_AUTH_KEY: ApiKeyDefaultValue From 043f6153104fff8069acd9028494eab035b2f803 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 2 Jun 2023 09:04:07 +0200 Subject: [PATCH 040/295] fix broker logger causing db errors (#102) --- .../V2__Broker_Server_Initial_DB_Model.sql | 5 +- .../services/logging/BrokerEventLogger.java | 59 ++++++++++--------- .../logging/BrokerEventLoggerTest.java | 59 +++++++++++++++++++ 3 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index ca6318756..56a78f69b 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -63,10 +63,7 @@ create type broker_event_status as enum ( 'OK', -- Failures - 'ERROR', - - -- E.g. refreshes, that resulted in no changes - 'UNCHANGED' + 'ERROR' ); create table broker_event_log diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index 6a3d9f2f9..d8b3abd59 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -31,50 +31,53 @@ public class BrokerEventLogger { public void logConnectorUpdateSuccess(DSLContext dsl, String connectorEndpoint, ConnectorChangeTracker changes) { - var logEntry = connectorUpdateEntry(dsl, connectorEndpoint); - logEntry.setEventStatus(getConnectorUpdateStatus(changes)); - logEntry.setUserMessage(changes.toString()); + var logEntry = logEntry( + dsl, + BrokerEventType.CONNECTOR_UPDATED, + connectorEndpoint, + changes.toString() + ); logEntry.insert(); } public void logConnectorUpdateFailure(DSLContext dsl, String connectorEndpoint, BrokerEventErrorMessage errorMessage) { - var logEntry = connectorUpdateEntry(dsl, connectorEndpoint); + var logEntry = logEntry( + dsl, + BrokerEventType.CONNECTOR_UPDATED, + connectorEndpoint, + errorMessage.message() + ); logEntry.setEventStatus(BrokerEventStatus.ERROR); - logEntry.setUserMessage(errorMessage.message()); logEntry.setErrorStack(errorMessage.stackTraceOrNull()); logEntry.insert(); } public void logConnectorUpdateStatusChange(DSLContext dsl, String connectorEndpoint, ConnectorOnlineStatus status) { - var logEntry = connectorUpdateEntry(dsl, connectorEndpoint); - switch (status) { - case ONLINE: - logEntry.setUserMessage("Connector is online: " + connectorEndpoint); - logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_ONLINE); - break; - case OFFLINE: - logEntry.setUserMessage("Connector is offline: " + connectorEndpoint); - logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE); - break; - default: - throw new IllegalArgumentException("Unknown status: " + status + " for connector: " + connectorEndpoint); - } + var logEntry = switch (status) { + case ONLINE -> logEntry( + dsl, + BrokerEventType.CONNECTOR_STATUS_CHANGE_ONLINE, + connectorEndpoint, + "Connector is online: " + connectorEndpoint + ); + case OFFLINE -> logEntry( + dsl, + BrokerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE, + connectorEndpoint, + "Connector is offline: " + connectorEndpoint + ); + default -> throw new IllegalArgumentException("Unknown status: " + status + " for connector: " + connectorEndpoint); + }; logEntry.insert(); } - private BrokerEventLogRecord connectorUpdateEntry(DSLContext dsl, String connectorEndpoint) { + private BrokerEventLogRecord logEntry(DSLContext dsl, BrokerEventType eventType, String connectorEndpoint, String userMessage) { var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setEvent(eventType); logEntry.setConnectorEndpoint(connectorEndpoint); logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setUserMessage(userMessage); return logEntry; } - - private BrokerEventStatus getConnectorUpdateStatus(ConnectorChangeTracker changes) { - if (changes.isEmpty()) { - return BrokerEventStatus.UNCHANGED; - } - - return BrokerEventStatus.OK; - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java new file mode 100644 index 000000000..3af3fba3e --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.logging; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; + +@ApiTest +@ExtendWith(EdcExtension.class) +public class BrokerEventLoggerTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( + BrokerServerExtension.KNOWN_CONNECTORS, "https://example.com/ids/data", + BrokerServerExtension.NUM_THREADS, "0" + ))); + } + + @Test + void testDataOfferWriter_allSortsOfUpdates() { + TEST_DATABASE.testTransaction(dsl -> { + var brokerEventLogger = new BrokerEventLogger(); + + // Test that insertions insert required fields and don't cause DB errors + brokerEventLogger.logConnectorUpdateSuccess(dsl, "https://example.com/ids/data", new ConnectorChangeTracker()); + brokerEventLogger.logConnectorUpdateFailure(dsl, "https://example.com/ids/data", new BrokerEventErrorMessage("Message", "Stacktrace")); + brokerEventLogger.logConnectorUpdateStatusChange(dsl, "https://example.com/ids/data", ConnectorOnlineStatus.ONLINE); + brokerEventLogger.logConnectorUpdateStatusChange(dsl, "https://example.com/ids/data", ConnectorOnlineStatus.OFFLINE); + }); + } +} From 332af2def8eac5f6f1d6d4e2090c00320ef93439 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Fri, 2 Jun 2023 11:11:08 +0200 Subject: [PATCH 041/295] feat: monitor crawling execution time (#99) * feat: log fetch execution time * feat: log fetch execution time * feat: log fetch execution time * refactor: db adjustments * refactor: BrokerExecutionTimeLogger * refactor: logExecutionTime --- .../V2__Broker_Server_Initial_DB_Model.sql | 12 ++++++ .../BrokerServerExtensionContextBuilder.java | 5 ++- .../logging/BrokerExecutionTimeLogger.java | 40 +++++++++++++++++++ .../services/refreshing/ConnectorUpdater.java | 21 ++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index 56a78f69b..45481e629 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -1,4 +1,6 @@ create type connector_online_status as enum ('ONLINE', 'OFFLINE'); +create type measurement_type as enum ('CONNECTOR_REFRESH'); +create type measurement_error_status as enum ('ERROR', 'OK'); create table connector ( @@ -79,4 +81,14 @@ create table broker_event_log duration_in_ms bigint ); +create table broker_execution_time_measurement +( + id serial primary key, + created_at timestamp with time zone not null, + connector_endpoint text not null, + duration_in_ms bigint not null, + type measurement_type not null, + error_status measurement_error_status not null +); + create index speedup on broker_event_log (connector_endpoint, asset_id, event_status); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 541d63282..786d57d5e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -29,6 +29,7 @@ import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; import de.sovity.edc.ext.brokerserver.services.queue.ThreadPool; @@ -86,6 +87,7 @@ public static BrokerServerExtensionContext buildContext( // Services var objectMapper = typeManager.getMapper(); var brokerEventLogger = new BrokerEventLogger(); + var brokerExecutionTimeLogger = new BrokerExecutionTimeLogger(); var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); var dataOfferRecordUpdater = new DataOfferRecordUpdater(); var dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); @@ -108,7 +110,8 @@ public static BrokerServerExtensionContext buildContext( connectorUpdateFailureWriter, connectorQueries, dslContextFactory, - monitor + monitor, + brokerExecutionTimeLogger ); var policyDtoBuilder = new PolicyDtoBuilder(objectMapper); var assetPropertyParser = new AssetPropertyParser(objectMapper); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java new file mode 100644 index 000000000..7743ac753 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.logging; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; + +/** + * Updates a single connector. + */ +@RequiredArgsConstructor +public class BrokerExecutionTimeLogger { + public void logExecutionTime(DSLContext dsl, String connectorEndpoint, long executionTimeInMs, MeasurementErrorStatus errorStatus) { + var logEntry = dsl.newRecord(Tables.BROKER_EXECUTION_TIME_MEASUREMENT); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setDurationInMs(executionTimeInMs); + logEntry.setType(MeasurementType.CONNECTOR_REFRESH); + logEntry.setErrorStatus(errorStatus); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.insert(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index 07d2cc505..203756ad3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -16,11 +16,16 @@ import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.time.StopWatch; import org.eclipse.edc.spi.monitor.Monitor; +import java.util.concurrent.TimeUnit; + /** * Updates a single connector. */ @@ -32,6 +37,7 @@ public class ConnectorUpdater { private final ConnectorQueries connectorQueries; private final DslContextFactory dslContextFactory; private final Monitor monitor; + private final BrokerExecutionTimeLogger brokerExecutionTimeLogger; /** * Updates single connector. @@ -39,8 +45,12 @@ public class ConnectorUpdater { * @param connectorEndpoint connector endpoint */ public void updateConnector(String connectorEndpoint) { + var executionTime = StopWatch.createStarted(); + var failed = false; + try { monitor.info("Updating connector: " + connectorEndpoint); + var dataOffers = dataOfferFetcher.fetch(connectorEndpoint); // Update connector in a single transaction @@ -49,6 +59,7 @@ public void updateConnector(String connectorEndpoint) { connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, dataOffers); }); } catch (Exception e) { + failed = true; try { // Update connector in a single transaction dslContextFactory.transaction(dsl -> { @@ -59,6 +70,16 @@ public void updateConnector(String connectorEndpoint) { e1.addSuppressed(e); monitor.severe("Failed updating connector as failed.", e1); } + } finally { + executionTime.stop(); + try { + var status = failed ? MeasurementErrorStatus.ERROR : MeasurementErrorStatus.OK; + dslContextFactory.transaction(dsl -> { + brokerExecutionTimeLogger.logExecutionTime(dsl, connectorEndpoint, executionTime.getTime(TimeUnit.MILLISECONDS), status); + }); + } catch (Exception e) { + monitor.severe("Failed logging connector update execution time.", e); + } } } } From 0e86097da18c59b2b5061b3df8a630506cf6b922 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 2 Jun 2023 14:19:47 +0200 Subject: [PATCH 042/295] chore: revert to 0.0.1-20230220.patch1 (MS8 patch1) (#103) * Reverted back to MS8 * test: ConnectorUpdate with MS8 * chore: compile time failure * chore: fix test failure --------- Co-authored-by: Tim Berthold --- connector/.env | 10 +- docker-compose.yaml | 31 +- .../build.gradle.kts | 2 +- .../db/migration/{V1__MS9.sql => V1__MS8.sql} | 303 ++++++++++++++++-- extensions/broker-server/build.gradle.kts | 12 +- .../brokerserver/BrokerServerExtension.java | 3 + .../BrokerServerExtensionContext.java | 14 +- .../BrokerServerExtensionContextBuilder.java | 9 +- .../services/ConnectorCreator.java | 4 + .../ConnectorUpdateFailureWriter.java | 5 +- .../ConnectorUpdateSuccessWriter.java | 2 +- .../refreshing/offers/DataOfferBuilder.java | 9 +- .../edc/ext/brokerserver/TestUtils.java | 20 +- .../ext/brokerserver/db/FlywayTestUtils.java | 42 +++ .../edc/ext/brokerserver/db/TestDatabase.java | 14 +- .../logging/BrokerEventLoggerTest.java | 25 +- .../refreshing/ConnectorUpdaterTest.java | 131 ++++++++ .../offers/DataOfferWriterTest.java | 20 +- .../offers/DataOfferWriterTestDataHelper.java | 5 + .../src/test/resources/logging.properties | 6 + gradle.properties | 2 +- 21 files changed, 561 insertions(+), 108 deletions(-) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V1__MS9.sql => V1__MS8.sql} (52%) create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java create mode 100644 extensions/broker-server/src/test/resources/logging.properties diff --git a/connector/.env b/connector/.env index bbbe7777d..864639827 100644 --- a/connector/.env +++ b/connector/.env @@ -10,9 +10,17 @@ EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * EDC_BROKER_SERVER_NUM_THREADS=3 +MY_EDC_BASE_PATH: "/backend" +MY_EDC_NAME_KEBAB_CASE: "broker" +MY_EDC_TITLE: "This will be unavailable starting Core EDC 0.1.0" +MY_EDC_DESCRIPTION: "This will be unavailable starting Core EDC 0.1.0" +MY_EDC_CURATOR_URL: "This will be unavailable starting Core EDC 0.1.0" +MY_EDC_CURATOR_NAME: "This will be unavailable starting Core EDC 0.1.0" +MY_EDC_MAINTAINER_URL: "This will be unavailable starting Core EDC 0.1.0" +MY_EDC_MAINTAINER_NAME: "This will be unavailable starting Core EDC 0.1.0" + # Deployment Settings MY_EDC_FQDN=missing-env-MY_EDC_FQDN -MY_EDC_BASE_PATH= MY_EDC_PROTOCOL=https:// # MY_EDC_NAME_KEBAB_CASE diff --git a/docker-compose.yaml b/docker-compose.yaml index d9477d1c3..34e891584 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,7 +6,7 @@ services: - '11000:80' environment: - EDC_UI_ACTIVE_PROFILE=broker - - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:11002/api/v1/management + - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:11002/backend/api/v1/management - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue - EDC_BROKER_SERVER_KNOWN_CONNECTORS=http://connector:11003/api/v1/ids/data broker: @@ -15,34 +15,29 @@ services: - broker-postgresql - connector environment: - MY_EDC_NAME_KEBAB_CASE: "broker" - MY_EDC_TITLE: "MDS Broker" - MY_EDC_DESCRIPTION: "Mobility Data Space Broker, collects and indexes all data offers of the Mobility Data Space (MDS)." - MY_EDC_CURATOR_URL: "https://mobility-dataspace.eu/" - MY_EDC_CURATOR_NAME: "Mobility Data Space" - MY_EDC_MAINTAINER_URL: "https://sovity.de" - MY_EDC_MAINTAINER_NAME: "sovity GmbH" - - # Local Dev Broker Overrides - EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" - EDC_BROKER_SERVER_NUM_THREADS: "1" - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" - # Data Management API Key EDC_API_AUTH_KEY: ApiKeyDefaultValue - MY_EDC_PROTOCOL: "http://" + # Deployment Configuration + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" MY_EDC_FQDN: "broker" - MY_EDC_IDS_BASE_URL: "http://broker:11003" - MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc + # Local Dev / Docker-Compose Config + MY_EDC_PROTOCOL: "http://" # We don't have TLS in the docker container + MY_EDC_IDS_BASE_URL: "http://broker:11003" # We have no reverse proxy removing ports in the docker compose EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' + + # Update connectors every 20s in dev + EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" + # No parallelism in dev + EDC_BROKER_SERVER_NUM_THREADS: "1" + # Hide offline data offers after 1 minute in dev + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" ports: - '11001:11001' - '11002:11002' diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 60382c570..fb294bd60 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -53,7 +53,7 @@ dependencies { implementation("${edcGroup}:transaction-local:${edcVersion}") implementation("org.postgresql:postgresql:${postgresVersion}") - implementation("org.flywaydb:flyway-core:${flywayVersion}") + api("org.flywaydb:flyway-core:${flywayVersion}") testImplementation("${edcGroup}:junit:${edcVersion}") } diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS9.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql similarity index 52% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS9.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql index 16e6c9ec4..9c5ff65f3 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS9.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql @@ -16,8 +16,7 @@ -- table: edc_asset CREATE TABLE IF NOT EXISTS edc_asset ( - asset_id VARCHAR NOT NULL, - created_at BIGINT NOT NULL, + asset_id VARCHAR NOT NULL, PRIMARY KEY (asset_id) ); @@ -34,11 +33,10 @@ COMMENT ON COLUMN edc_asset_dataaddress.properties IS 'DataAddress properties se -- table: edc_asset_property CREATE TABLE IF NOT EXISTS edc_asset_property ( - asset_id_fk VARCHAR NOT NULL, - property_name VARCHAR NOT NULL, - property_value VARCHAR NOT NULL, - property_type VARCHAR NOT NULL, - property_is_private BOOLEAN NOT NULL, + asset_id_fk VARCHAR NOT NULL, + property_name VARCHAR NOT NULL, + property_value VARCHAR NOT NULL, + property_type VARCHAR NOT NULL, PRIMARY KEY (asset_id_fk, property_name), FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE ); @@ -49,12 +47,33 @@ COMMENT ON COLUMN edc_asset_property.property_value IS 'Asset property value'; COMMENT ON COLUMN edc_asset_property.property_type IS 'Asset property class name'; -COMMENT ON COLUMN edc_asset_property.property_is_private IS - 'Asset property private flag'; CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value ON edc_asset_property (property_name, property_value); +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_asset + ADD created_at BIGINT; + +UPDATE edc_asset +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_asset + ALTER COLUMN created_at SET NOT NULL; + -- -- Copyright (c) 2022 Daimler TSS GmbH -- @@ -73,14 +92,68 @@ CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value -- only intended for and tested with H2 and Postgres! CREATE TABLE IF NOT EXISTS edc_contract_definitions ( - created_at BIGINT NOT NULL, contract_definition_id VARCHAR NOT NULL, access_policy_id VARCHAR NOT NULL, contract_policy_id VARCHAR NOT NULL, - assets_selector JSON NOT NULL, + selector_expression JSON NOT NULL, PRIMARY KEY (contract_definition_id) ); +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_definitions + ADD created_at BIGINT; + +UPDATE edc_contract_definitions +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_contract_definitions + ALTER COLUMN created_at SET NOT NULL; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- + +ALTER TABLE edc_contract_definitions + ADD validity BIGINT; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_contract_definitions +SET validity=60 * 60 * 24 * 365 +WHERE validity IS NULL; + -- Statements are designed for and tested with Postgres only! CREATE TABLE IF NOT EXISTS edc_lease @@ -123,13 +196,11 @@ CREATE TABLE IF NOT EXISTS edc_contract_negotiation id VARCHAR NOT NULL CONSTRAINT contract_negotiation_pk PRIMARY KEY, - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, correlation_id VARCHAR, counterparty_id VARCHAR NOT NULL, counterparty_address VARCHAR NOT NULL, - protocol VARCHAR NOT NULL, - type VARCHAR NOT NULL, + protocol VARCHAR DEFAULT 'ids-multipart'::CHARACTER VARYING NOT NULL, + type INTEGER DEFAULT 0 NOT NULL, state INTEGER DEFAULT 0 NOT NULL, state_count INTEGER DEFAULT 0, state_timestamp BIGINT, @@ -138,7 +209,6 @@ CREATE TABLE IF NOT EXISTS edc_contract_negotiation CONSTRAINT contract_negotiation_contract_agreement_id_fk REFERENCES edc_contract_agreement, contract_offers JSON, - callback_addresses JSON, trace_context JSON, lease_id VARCHAR CONSTRAINT contract_negotiation_lease_lease_id_fk @@ -163,6 +233,88 @@ CREATE UNIQUE INDEX IF NOT EXISTS contract_negotiation_id_uindex CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex ON edc_contract_agreement (agr_id); +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_negotiation + ADD created_at BIGINT, + ADD updated_at BIGINT; + +UPDATE edc_contract_negotiation +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; +UPDATE edc_contract_negotiation +SET updated_at=created_at; + +ALTER TABLE edc_contract_negotiation + ALTER COLUMN created_at SET NOT NULL; +ALTER TABLE edc_contract_negotiation + ALTER COLUMN updated_at SET NOT NULL; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +UPDATE edc_contract_negotiation +SET contract_offers = co.contract_offers_edited +FROM (SELECT cn.id, + jsonb_agg( + jsonb_set( + jsonb_set( + elems, + '{contractStart}', + to_json(to_char(to_timestamp(created_at / 1000) AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ), + '{contractEnd}', + to_json(to_char(to_timestamp((created_at / 1000) + 60 * 60 * 24 * 365) AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ) + )::json as contract_offers_edited + FROM edc_contract_negotiation cn, + jsonb_array_elements(cn.contract_offers::jsonb) elems + GROUP BY cn.id) co +WHERE edc_contract_negotiation.id = co.id; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - initial API and implementation for DataplaneInstances +-- +-- +CREATE TABLE IF NOT EXISTS edc_data_plane_instance +( + id VARCHAR NOT NULL, + data JSON NOT NULL, + PRIMARY KEY (id) +); + -- -- Copyright (c) 2022 ZF Friedrichshafen AG -- @@ -182,7 +334,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex CREATE TABLE IF NOT EXISTS edc_policydefinitions ( policy_id VARCHAR NOT NULL, - created_at BIGINT NOT NULL, permissions JSON, prohibitions JSON, duties JSON, @@ -204,6 +355,28 @@ COMMENT ON COLUMN edc_policydefinitions.policy_type IS 'Java PolicyType serializ CREATE UNIQUE INDEX IF NOT EXISTS edc_policydefinitions_id_uindex ON edc_policydefinitions (policy_id); +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_policydefinitions + ADD created_at BIGINT; + +UPDATE edc_policydefinitions +SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_policydefinitions + ALTER COLUMN created_at SET NOT NULL; + -- Statements are designed for and tested with Postgres only! CREATE TABLE IF NOT EXISTS edc_lease @@ -225,21 +398,18 @@ CREATE TABLE IF NOT EXISTS edc_transfer_process transferprocess_id VARCHAR NOT NULL CONSTRAINT transfer_process_pk PRIMARY KEY, - type VARCHAR NOT NULL, - state INTEGER NOT NULL, - state_count INTEGER DEFAULT 0 NOT NULL, - state_time_stamp BIGINT, - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, - trace_context JSON, - error_detail VARCHAR, - resource_manifest JSON, - provisioned_resource_set JSON, - content_data_address JSON, - deprovisioned_resources JSON, - private_properties JSON, - callback_addresses JSON, - lease_id VARCHAR + type VARCHAR NOT NULL, + state INTEGER NOT NULL, + state_count INTEGER DEFAULT 0 NOT NULL, + state_time_stamp BIGINT, + created_time_stamp BIGINT, + trace_context JSON, + error_detail VARCHAR, + resource_manifest JSON, + provisioned_resource_set JSON, + content_data_address JSON, + deprovisioned_resources JSON, + lease_id VARCHAR CONSTRAINT transfer_process_lease_lease_id_fk REFERENCES edc_lease ON DELETE SET NULL @@ -273,6 +443,7 @@ CREATE TABLE IF NOT EXISTS edc_data_request data_destination JSON NOT NULL, managed_resources BOOLEAN DEFAULT TRUE, properties JSON, + transfer_type JSON, transfer_process_id VARCHAR NOT NULL CONSTRAINT data_request_transfer_process_id_fk REFERENCES edc_transfer_process @@ -283,8 +454,76 @@ COMMENT ON COLUMN edc_data_request.data_destination IS 'DataAddress serialized a COMMENT ON COLUMN edc_data_request.properties IS 'java Map serialized as JSON'; +COMMENT ON COLUMN edc_data_request.transfer_type IS 'TransferType serialized as JSON'; + + CREATE UNIQUE INDEX IF NOT EXISTS data_request_id_uindex ON edc_data_request (datarequest_id); CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex ON edc_lease (lease_id); + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_transfer_process + RENAME COLUMN created_time_stamp TO created_at; + +UPDATE edc_transfer_process +SET created_at = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 +WHERE created_at = NULL; + +ALTER TABLE edc_transfer_process + ADD updated_at BIGINT; + +UPDATE edc_transfer_process +SET updated_at=created_at; + +ALTER TABLE edc_transfer_process + ALTER COLUMN updated_at SET NOT NULL; +ALTER TABLE edc_transfer_process + ALTER COLUMN created_at SET NOT NULL; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +ALTER TABLE edc_transfer_process + ADD transferprocess_properties JSON; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_transfer_process +SET transferprocess_properties = '{}'::json +WHERE transferprocess_properties IS NULL; diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 5a0893849..0067b577c 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -22,12 +22,8 @@ dependencies { compileOnly("org.projectlombok:lombok:1.18.28") implementation("org.apache.commons:commons-lang3:3.12.0") - implementation("${edcGroup}:control-plane-core:${edcVersion}") + implementation("${edcGroup}:control-plane-spi:${edcVersion}") implementation("${edcGroup}:management-api-configuration:${edcVersion}") - implementation("${edcGroup}:ids-spi:${edcVersion}") - implementation("${edcGroup}:ids-api-multipart-dispatcher-v1:${edcVersion}") - implementation("${edcGroup}:ids-api-configuration:${edcVersion}") - implementation("${edcGroup}:ids-jsonld-serdes:${edcVersion}") api(project(":extensions:broker-server-postgres-flyway-jooq")) api("${sovityEdcGroup}:wrapper-broker-api:${sovityEdcExtensionsVersion}") { isChanging = true } @@ -42,8 +38,12 @@ dependencies { testImplementation("${edcGroup}:junit:${edcVersion}") testImplementation("${edcGroup}:http:${edcVersion}") testImplementation("${edcGroup}:iam-mock:${edcVersion}") - testImplementation("io.rest-assured:rest-assured:${restAssured}") + testImplementation("${edcGroup}:ids:${edcVersion}") + testImplementation("${edcGroup}:management-api:${edcVersion}") + testImplementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") + testImplementation("${edcGroup}:configuration-filesystem:${edcVersion}") testImplementation("${sovityEdcGroup}:client:${sovityEdcExtensionsVersion}") + testImplementation("io.rest-assured:rest-assured:${restAssured}") testImplementation("org.testcontainers:testcontainers:${testcontainersVersion}") testImplementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 099fc0659..ed8741d93 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -70,6 +70,9 @@ public void initialize(ServiceExtensionContext context) { catalogService ); + // This is a hack for tests, so we can access the running context from tests. + BrokerServerExtensionContext.instance = services; + var managementApiGroup = managementApiConfiguration.getContextAlias(); webService.registerResource(managementApiGroup, services.brokerServerResource()); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index 992718b86..538971fe0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -15,6 +15,8 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; +import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; +import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; @@ -26,6 +28,16 @@ */ public record BrokerServerExtensionContext( BrokerServerResource brokerServerResource, - BrokerServerInitializer brokerServerInitializer + BrokerServerInitializer brokerServerInitializer, + + // Required for Integration Tests + ConnectorUpdater connectorUpdater, + ConnectorCreator connectorCreator ) { + /** + * This is a hack for our tests. + *

+ * Right now we have no good way to access the context from tests. + */ + public static BrokerServerExtensionContext instance; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 786d57d5e..a29efae70 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -100,7 +100,7 @@ public static BrokerServerExtensionContext buildContext( var dataOfferPatchApplier = new DataOfferPatchApplier(); var dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger, dataOfferWriter); - var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger); + var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger, monitor); var contractOfferFetcher = new ContractOfferFetcher(catalogService); var fetchedDataOfferBuilder = new DataOfferBuilder(objectMapper); var dataOfferFetcher = new DataOfferFetcher(contractOfferFetcher, fetchedDataOfferBuilder); @@ -151,6 +151,11 @@ public static BrokerServerExtensionContext buildContext( connectorApiService, catalogApiService ); - return new BrokerServerExtensionContext(brokerServerResource, brokerServerInitializer); + return new BrokerServerExtensionContext( + brokerServerResource, + brokerServerInitializer, + connectorUpdater, + connectorCreator + ); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index 08efa524d..72f841555 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -30,6 +30,10 @@ public class ConnectorCreator { private final ConnectorQueries connectorQueries; + public void addConnector(DSLContext dsl, String connectorEndpoint) { + addConnectors(dsl, List.of(connectorEndpoint)); + } + public void addConnectors(DSLContext dsl, List connectorEndpoints) { // Don't create connectors that already exist var existingConnectors = connectorQueries.findExistingConnectors(dsl, connectorEndpoints); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java index caa70f00c..5e1f2856c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; import org.jooq.DSLContext; import java.time.OffsetDateTime; @@ -27,13 +28,15 @@ @RequiredArgsConstructor public class ConnectorUpdateFailureWriter { private final BrokerEventLogger brokerEventLogger; + private final Monitor monitor; public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Throwable e) { // Log Status Change and set status to offline if necessary - if (connector.getOnlineStatus() == ConnectorOnlineStatus.ONLINE) { + if (connector.getOnlineStatus() == ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.OFFLINE); brokerEventLogger.logConnectorUpdateFailure(dsl, connector.getEndpoint(), getFailureMessage(e)); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + monitor.info("Connector is offline: " + connector.getEndpoint(), e); } connector.setLastRefreshAttemptAt(OffsetDateTime.now()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index acca51f51..9a93008cc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -39,7 +39,7 @@ public void handleConnectorOnline( var now = OffsetDateTime.now(); // Log Status Change and set status to online if necessary - if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE) { + if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE || connector.getLastRefreshAttemptAt() == null) { brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.ONLINE); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java index 9402271fe..4e5e7747e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java @@ -44,14 +44,15 @@ public class DataOfferBuilder { public Collection deduplicateContractOffers(Collection contractOffers) { return groupByAssetId(contractOffers) .stream() - .map(offers -> buildFetchedDataOffer(offers.get(0).getAssetId(), offers)) + .map(offers -> buildFetchedDataOffer(offers.get(0).getAsset(), offers)) .toList(); } @NotNull - private FetchedDataOffer buildFetchedDataOffer(String assetId, List offers) { + private FetchedDataOffer buildFetchedDataOffer(Asset asset, List offers) { var dataOffer = new FetchedDataOffer(); - dataOffer.setAssetId(assetId); + dataOffer.setAssetId(asset.getId()); + dataOffer.setAssetPropertiesJson(getAssetPropertiesJson(asset)); dataOffer.setContractOffers(buildFetchedDataOfferContractOffers(offers)); return dataOffer; } @@ -73,7 +74,7 @@ private FetchedDataOfferContractOffer buildFetchedDataOfferContractOffer(Contrac } private Collection> groupByAssetId(Collection contractOffers) { - return contractOffers.stream().collect(groupingBy(ContractOffer::getAssetId)).values(); + return contractOffers.stream().collect(groupingBy(offer -> offer.getAsset().getId())).values(); } @NotNull diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 9935e720b..71c94fcfc 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -17,6 +17,7 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import org.eclipse.edc.protocol.ids.api.configuration.IdsApiConfigurationExtension; import org.jetbrains.annotations.NotNull; import java.util.HashMap; @@ -28,9 +29,15 @@ public class TestUtils { private static final int DATA_PORT = getFreePort(); + private static final int PROTOCOL_PORT = getFreePort(); + private static final String DATA_PATH = "/api/v1/data"; + private static final String PROTOCOL_PATH = "/api/v1/ids"; public static final String MANAGEMENT_API_KEY = "123456"; - public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + "/api/v1/data"; - public static final String IDS_ENDPOINT = "http://localhost:" + DATA_PORT + "/api/v1/data/ids"; + public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + DATA_PATH; + + + public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; + public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH + "/data"; @NotNull public static Map createConfiguration( @@ -41,14 +48,19 @@ public static Map createConfiguration( config.put("web.http.port", String.valueOf(getFreePort())); config.put("web.http.path", "/api"); config.put("web.http.management.port", String.valueOf(DATA_PORT)); - config.put("web.http.management.path", "/api/v1/data"); + config.put("web.http.management.path", DATA_PATH); + config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); + config.put("web.http.protocol.path", PROTOCOL_PATH); config.put("edc.api.auth.key", MANAGEMENT_API_KEY); - config.put("edc.ids.endpoint", IDS_ENDPOINT); + config.put("edc.ids.endpoint", PROTOCOL_ENDPOINT); + config.put(IdsApiConfigurationExtension.IDS_WEBHOOK_ADDRESS, PROTOCOL_HOST); + config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); config.put(PostgresFlywayExtension.JDBC_URL, testDatabase.getJdbcUrl()); config.put(PostgresFlywayExtension.JDBC_USER, testDatabase.getJdbcUser()); config.put(PostgresFlywayExtension.JDBC_PASSWORD, testDatabase.getJdbcPassword()); config.put(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, "true"); config.put(PostgresFlywayExtension.FLYWAY_CLEAN, "true"); + config.put(BrokerServerExtension.NUM_THREADS, "0"); config.putAll(getCoreEdcJdbcConfig(testDatabase)); config.putAll(additionalConfigProperties); return config; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java new file mode 100644 index 000000000..416fde00e --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.db; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.eclipse.edc.monitor.logger.LoggerMonitor; +import org.eclipse.edc.spi.system.configuration.Config; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FlywayTestUtils { + + public static void migrate(TestDatabase testDatabase) { + var monitor = new LoggerMonitor(); + var config = mock(Config.class); + when(config.getBoolean(eq(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE), any())).thenReturn(true); + when(config.getBoolean(eq(PostgresFlywayExtension.FLYWAY_CLEAN), any())).thenReturn(true); + + var flywayFactory = new FlywayFactory(config); + var dataSource = testDatabase.getDataSource(); + var flyway = flywayFactory.setupFlyway(dataSource); + var flywayMigrator = new FlywayMigrator(flyway, config, monitor); + flywayMigrator.migrateAndRepair(); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index e5dab25b4..9e074fbd1 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.BeforeAllCallback; import java.util.function.Consumer; +import javax.sql.DataSource; public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { String getJdbcUrl(); @@ -34,11 +35,20 @@ public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { * @return {@link DslContextFactory} */ default DslContextFactory getDslContextFactory() { - var jdbcCredentials = new JdbcCredentials(getJdbcUrl(), getJdbcUser(), getJdbcPassword()); - var dataSource = DataSourceFactory.fromJdbcCredentials(jdbcCredentials); + var dataSource = getDataSource(); return new DslContextFactory(dataSource); } + /** + * Returns a {@link DataSource} to the test database + * + * @return {@link DataSource} + */ + default DataSource getDataSource() { + var jdbcCredentials = new JdbcCredentials(getJdbcUrl(), getJdbcUser(), getJdbcPassword()); + return DataSourceFactory.fromJdbcCredentials(jdbcCredentials); + } + /** * Runs given code within a test transaction. *
diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java index 3af3fba3e..ddaacf050 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java @@ -14,34 +14,21 @@ package de.sovity.edc.ext.brokerserver.services.logging; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; - -@ApiTest -@ExtendWith(EdcExtension.class) -public class BrokerEventLoggerTest { - +class BrokerEventLoggerTest { @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.KNOWN_CONNECTORS, "https://example.com/ids/data", - BrokerServerExtension.NUM_THREADS, "0" - ))); + @BeforeAll + static void setup() { + FlywayTestUtils.migrate(TEST_DATABASE); } @Test diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java new file mode 100644 index 000000000..b3eda5549 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtensionContext; +import de.sovity.edc.ext.brokerserver.TestUtils; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.asset.AssetSelectorExpression; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class ConnectorUpdaterTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + } + + @Test + void testConnectorUpdate( + AssetService assetService, + PolicyDefinitionStore policyDefinitionStore, + ContractDefinitionStore contractDefinitionStore + ) { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var connectorUpdater = BrokerServerExtensionContext.instance.connectorUpdater(); + var connectorCreator = BrokerServerExtensionContext.instance.connectorCreator(); + String connectorEndpoint = TestUtils.PROTOCOL_ENDPOINT; + + createAlwaysTruePolicyDefinition(policyDefinitionStore); + createAlwaysTrueContractDefinition(contractDefinitionStore); + createAsset(assetService, "test-asset-1", "Test Asset 1"); + connectorCreator.addConnector(dsl, connectorEndpoint); + + // act + connectorUpdater.updateConnector(connectorEndpoint); + + // assert + var connectors = dsl.selectFrom(Tables.CONNECTOR).stream().toList(); + assertThat(connectors.get(0).getOnlineStatus()).isEqualTo(ConnectorOnlineStatus.ONLINE); + assertThat(connectors.get(0).getEndpoint()).isEqualTo(connectorEndpoint); + + var dataOffers = dsl.selectFrom(Tables.DATA_OFFER).stream().toList(); + assertThat(dataOffers).hasSize(1); + + var dataOffer = dataOffers.get(0); + assertThat(dataOffer.getAssetId()).isEqualTo("test-asset-1"); + assertThat(dataOffer.getAssetProperties().data()).contains("Test Asset 1"); + + var contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().toList(); + assertThat(contractOffers).hasSize(1); + }); + } + + private void createAlwaysTruePolicyDefinition(PolicyDefinitionStore policyDefinitionStore) { + var policyDefinition = PolicyDefinition.Builder.newInstance() + .id("always-true") + .policy(Policy.Builder.newInstance().build()) + .build(); + policyDefinitionStore.save(policyDefinition); + } + + public void createAlwaysTrueContractDefinition(ContractDefinitionStore contractDefinitionStore) { + var contractDefinition = ContractDefinition.Builder.newInstance() + .id("always-true-cd") + .contractPolicyId("always-true") + .accessPolicyId("always-true") + .selectorExpression(AssetSelectorExpression.SELECT_ALL) + .validity(1000) //else throws "validity must be strictly positive" + .build(); + contractDefinitionStore.save(contractDefinition); + } + + private void createAsset( + AssetService assetService, + String assetId, + String assetName + ) { + var asset = Asset.Builder.newInstance() + .id(assetId) + .property(AssetProperty.ASSET_ID, assetId) + .property(AssetProperty.ASSET_NAME, assetName) + .build(); + var dataAddress = DataAddress.Builder.newInstance() + .properties(Map.of( + "type", "HttpData", + "baseUrl", "https://jsonplaceholder.typicode.com/todos/1" + )) + .build(); + assetService.create(asset, dataAddress); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java index 30e7ea48f..78412a0bd 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; @@ -22,34 +22,24 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Do; import org.assertj.core.data.TemporalUnitLessThanOffset; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.List; -import java.util.Map; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; -@ApiTest -@ExtendWith(EdcExtension.class) class DataOfferWriterTest { @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.KNOWN_CONNECTORS, "https://example.com/ids/data", - BrokerServerExtension.NUM_THREADS, "0" - ))); + @BeforeAll + static void setup() { + FlywayTestUtils.migrate(TEST_DATABASE); } @Test diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java index b20bfa2ac..65be85c1d 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java @@ -15,8 +15,10 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; import org.apache.commons.lang3.Validate; @@ -65,6 +67,9 @@ public void existing(Do dataOffer) { } public void initialize(DSLContext dsl) { + var connectorQueries = new ConnectorQueries(); + var connectorCreator = new ConnectorCreator(connectorQueries); + connectorCreator.addConnector(dsl, connectorEndpoint); dsl.batchInsert(existingDataOffers).execute(); dsl.batchInsert(existingContractOffers).execute(); } diff --git a/extensions/broker-server/src/test/resources/logging.properties b/extensions/broker-server/src/test/resources/logging.properties new file mode 100644 index 000000000..d2212b2a2 --- /dev/null +++ b/extensions/broker-server/src/test/resources/logging.properties @@ -0,0 +1,6 @@ +.level=ALL +org.eclipse.edc.level=ALL +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=[%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS] [%4$-7s] %5$s%6$s%n \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c59cbdfd8..cce85f653 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ sovityEdcGroup=de.sovity.edc # Eclipse EDC edcGroup=org.eclipse.edc -edcVersion=0.0.1-milestone-9 +edcVersion=0.0.1-20230220.patch1 # Other Dependencies assertj=3.23.1 From 43aeda70b1e3ca2e448a40b3f61406e6c983b9c7 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 2 Jun 2023 14:45:35 +0200 Subject: [PATCH 043/295] fix: connector .env (#106) --- connector/.env | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/connector/.env b/connector/.env index 864639827..99482cb03 100644 --- a/connector/.env +++ b/connector/.env @@ -10,14 +10,14 @@ EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * EDC_BROKER_SERVER_NUM_THREADS=3 -MY_EDC_BASE_PATH: "/backend" -MY_EDC_NAME_KEBAB_CASE: "broker" -MY_EDC_TITLE: "This will be unavailable starting Core EDC 0.1.0" -MY_EDC_DESCRIPTION: "This will be unavailable starting Core EDC 0.1.0" -MY_EDC_CURATOR_URL: "This will be unavailable starting Core EDC 0.1.0" -MY_EDC_CURATOR_NAME: "This will be unavailable starting Core EDC 0.1.0" -MY_EDC_MAINTAINER_URL: "This will be unavailable starting Core EDC 0.1.0" -MY_EDC_MAINTAINER_NAME: "This will be unavailable starting Core EDC 0.1.0" +MY_EDC_BASE_PATH="/backend" +MY_EDC_NAME_KEBAB_CASE="broker" +MY_EDC_TITLE="This will be unavailable starting Core EDC 0.1.0" +MY_EDC_DESCRIPTION="This will be unavailable starting Core EDC 0.1.0" +MY_EDC_CURATOR_URL="This will be unavailable starting Core EDC 0.1.0" +MY_EDC_CURATOR_NAME="This will be unavailable starting Core EDC 0.1.0" +MY_EDC_MAINTAINER_URL="This will be unavailable starting Core EDC 0.1.0" +MY_EDC_MAINTAINER_NAME="This will be unavailable starting Core EDC 0.1.0" # Deployment Settings MY_EDC_FQDN=missing-env-MY_EDC_FQDN From 1486f899a0384e6eccefb80e93bfb5ea569178c4 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 2 Jun 2023 14:52:19 +0200 Subject: [PATCH 044/295] fix: connector .env (2) (#107) --- connector/.env | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/connector/.env b/connector/.env index 99482cb03..5a4201fb6 100644 --- a/connector/.env +++ b/connector/.env @@ -10,14 +10,14 @@ EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * EDC_BROKER_SERVER_NUM_THREADS=3 -MY_EDC_BASE_PATH="/backend" -MY_EDC_NAME_KEBAB_CASE="broker" -MY_EDC_TITLE="This will be unavailable starting Core EDC 0.1.0" -MY_EDC_DESCRIPTION="This will be unavailable starting Core EDC 0.1.0" -MY_EDC_CURATOR_URL="This will be unavailable starting Core EDC 0.1.0" -MY_EDC_CURATOR_NAME="This will be unavailable starting Core EDC 0.1.0" -MY_EDC_MAINTAINER_URL="This will be unavailable starting Core EDC 0.1.0" -MY_EDC_MAINTAINER_NAME="This will be unavailable starting Core EDC 0.1.0" +MY_EDC_BASE_PATH=/backend +MY_EDC_NAME_KEBAB_CASE=broker +MY_EDC_TITLE=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_DESCRIPTION=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_CURATOR_URL=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_CURATOR_NAME=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_MAINTAINER_URL=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_MAINTAINER_NAME=This will be unavailable starting Core EDC 0.1.0 # Deployment Settings MY_EDC_FQDN=missing-env-MY_EDC_FQDN From 645ae83f6ee3447e9dd6465175e9614cadfccd8e Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 2 Jun 2023 15:00:47 +0200 Subject: [PATCH 045/295] fix: connector .env (3) --- connector/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connector/.env b/connector/.env index 5a4201fb6..032ba9113 100644 --- a/connector/.env +++ b/connector/.env @@ -14,9 +14,9 @@ MY_EDC_BASE_PATH=/backend MY_EDC_NAME_KEBAB_CASE=broker MY_EDC_TITLE=This will be unavailable starting Core EDC 0.1.0 MY_EDC_DESCRIPTION=This will be unavailable starting Core EDC 0.1.0 -MY_EDC_CURATOR_URL=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_CURATOR_URL=http://this-will-be-unavailable-starting-core-edc-0-1-0 MY_EDC_CURATOR_NAME=This will be unavailable starting Core EDC 0.1.0 -MY_EDC_MAINTAINER_URL=This will be unavailable starting Core EDC 0.1.0 +MY_EDC_MAINTAINER_URL=http://this-will-be-unavailable-starting-core-edc-0-1-0 MY_EDC_MAINTAINER_NAME=This will be unavailable starting Core EDC 0.1.0 # Deployment Settings From 7260241ebd5bd7989ea1100d0996e6f20e09d6c0 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 5 Jun 2023 07:31:27 +0200 Subject: [PATCH 046/295] chore: prepare documentation for PoC release (#108) * chore: prepare documentation for PoC release * try remove spammy log messages --- .github/ISSUE_TEMPLATE/release.md | 39 +++++++++ CHANGELOG.md | 20 +++++ README.md | 83 ++++++++++++++++++- connector/.env | 76 +++++++---------- .../src/main/resources/logging.properties | 1 + docker-compose.yaml | 42 ++-------- 6 files changed, 181 insertions(+), 80 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/release.md diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md new file mode 100644 index 000000000..704db8c26 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.md @@ -0,0 +1,39 @@ +--- +name: Release +about: Create an issue to track a release process. +title: "Release x.x.x" +labels: ["task/release", "scope/mds"] +assignees: "" +--- + +# Release + +## Work Breakdown + +Feel free to edit this release checklist in-progress depending on what tasks need to be done: + +- [ ] Release [edc-ui](https://github.com/sovity/edc-ui), this might require several steps. +- [ ] Release [edc-extensions](https://github.com/sovity/edc-extensions), this might require several steps. +- [ ] Update the CHANGELOG.md. + - [ ] Decide a release version depending on major/minor/patch changes. + - [ ] Add a clean `Unreleased` version and rename the old section to the release version. + - [ ] Remove empty sections from the release version's patch notes. + - [ ] Write or review the `Deployment Migration Notes` section. + - [ ] Write or review a release summary. +- [ ] Set the release versions in the [docker-compose's .env file](.env). +- [ ] Set the release versions in the [Deployment Section of our README.md](README.md#deployment). Use text instead of a + link for the broker server version, as the package version does only exist after the release. +- [ ] Commit those changes in a `release-prep` PR. +- [ ] Manually test the release. +- [ ] Create a release and re-use the changelog section as release description. +- [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). +- [ ] Revert the versions in the [docker-compose's .env file](.env) back to the snapshot/nightly versions of the + components. +- [ ] Change the broker server release version references in + the [Deployment Section of our README.md](README.md#deployment) to links to the package version. +- [ ] Change the broker server release version references in the Release Description to links to the package version. +- [ ] Commit those changes in a `release-cleanup` PR. +- [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md]. Apply changes where it + makes sense. +- [ ] Notify the deployment team with the release notes, which should now contain both product changes and a + configuration migration guide. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f2aa42e53..5d6355f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -6,8 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - yyyy-mm-dd +### Deployment Migration Notes + ### Major ### Minor ### Patch + +## [v0.0.1] Broker PoC Release - 2023-06-02 + +Initial Broker PoC Release with a minimalistic feature set. + +### Deployment Migration Notes + +Please view the [README.md](README.md#deployment) for initial deployment instructions. + +### Major + +- Implemented a Broker PoC with EDC MS8: + - Periodic Crawling of Connectors + - Query Data Offers via UI + - Query Connectors via UI + - Persistence of Connector Status Updates + - Persistence of Crawling Execution Times diff --git a/README.md b/README.md index 9e9a61521..94c15923b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ Broker Backend & EDC Extensions.

Table of Contents
  1. About The Project
  2. -
  3. Requirements
  4. +
  5. Development
  6. +
  7. Releasing
  8. +
  9. Deployment
  10. License
  11. Contact
@@ -46,7 +48,7 @@ This IDS Broker is written on basis of the EDC and should be used in tandem with

(back to top)

-## Requirements +## Development For development, access to the GitHub Maven Registry is required. @@ -60,6 +62,83 @@ gpr.key={your github pat with packages.read}

(back to top)

+## Releasing + +Create an issue using the [release template](.github/ISSUE_TEMPLATE/release.md) and follow the instructions. + +

(back to top)

+ +## Deployment + +### Deployment Units + +| Deployment Unit | Version / Details | +|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | +| Postgresql | 15 or compatible version | +| Broker Backend | broker-server-ce:0.0.1 | +| Broker UI | [edc-ui:0.0.1-milestone-8-sovity5](https://github.com/sovity/edc-ui/pkgs/container/edc-ui/91758285?tag=0.0.1-milestone-8-sovity5) | + +### Configuration + +There is a [docker-compose.yaml](docker-compose.yaml) to try out the broker locally. However, a productive release will +require a few more configuration options, so you should only use it to check if the released version is roughly working +or if it's broken. + +#### Reverse Proxy Configuration + +- The broker is meant to be served via TLS/HTTPS. +- The broker is meant to be deployed with a reverse proxy merging the following ports: + - The UI's `80` port. + - The Backend's `11002` port. + - The Backend's `11003` port. + +#### Backend Configuration + +A productive configuration will require you to join a DAPS. + +For that you will need a SKI/AKI ClientID. Please refer +to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-extensions/tree/main/docs/getting-started#faq) +on how to generate one. + +```yaml +# Required: Fully Qualified Domain Name +MY_EDC_FQDN: "example.com" + +# Required: DB +MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc +MY_EDC_JDBC_USER: edc +MY_EDC_JDBC_PASSWORD: edc + +# Required: List of EDCs to fetch +EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/ids/data,https://connector-b/ids/data" + +# Required: DAPS credentials +EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' +EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' +EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' +EDC_KEYSTORE: '_your keystore file_' # Needs to be available as file in the running container +EDC_KEYSTORE_PASSWORD: '_your keystore password_' + +# Required: Management API Key +EDC_API_AUTH_KEY: "ApiKeyDefaultValue" +``` + +#### UI Configuration + +```yaml +# Required: Profile +EDC_UI_ACTIVE_PROFILE: broker + +# Required: Management API URL +EDC_UI_DATA_MANAGEMENT_API_URL: https://my-broker.com/backend/api/v1/management + +# Required: Management API Key +EDC_API_AUTH_KEY: "ApiKeyDefaultValue" +``` + +

(back to top)

+ ## License Distributed under the Apache 2.0 License. See `LICENSE` for more information. diff --git a/connector/.env b/connector/.env index 032ba9113..35a81de91 100644 --- a/connector/.env +++ b/connector/.env @@ -4,48 +4,39 @@ # - KEY=Value will become KEY=${KEY:-"Value"}, so that ENV Vars can be overwritten by parent docker-compose.yaml. # - Watch out for escaping issues as values will be surrounded by quotes, and dollar signs must be escaped. -# Broker Server Related -EDC_BROKER_SERVER_KNOWN_CONNECTORS= -EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D -EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * -EDC_BROKER_SERVER_NUM_THREADS=3 +# =========================================================== +# Available Broker Server Config +# =========================================================== -MY_EDC_BASE_PATH=/backend -MY_EDC_NAME_KEBAB_CASE=broker -MY_EDC_TITLE=This will be unavailable starting Core EDC 0.1.0 -MY_EDC_DESCRIPTION=This will be unavailable starting Core EDC 0.1.0 -MY_EDC_CURATOR_URL=http://this-will-be-unavailable-starting-core-edc-0-1-0 -MY_EDC_CURATOR_NAME=This will be unavailable starting Core EDC 0.1.0 -MY_EDC_MAINTAINER_URL=http://this-will-be-unavailable-starting-core-edc-0-1-0 -MY_EDC_MAINTAINER_NAME=This will be unavailable starting Core EDC 0.1.0 - -# Deployment Settings +# Fully Qualified Domain Name (e.g. example.com) MY_EDC_FQDN=missing-env-MY_EDC_FQDN -MY_EDC_PROTOCOL=https:// -# MY_EDC_NAME_KEBAB_CASE -EDC_CONNECTOR_NAME=$MY_EDC_NAME_KEBAB_CASE -EDC_IDS_ID=urn:connector:$MY_EDC_NAME_KEBAB_CASE +# Postgres Database Connection +MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url +MY_EDC_JDBC_USER=missing-postgresql-user +MY_EDC_JDBC_PASSWORD=missing-postgresql-password -# MY_EDC_TITLE -EDC_IDS_TITLE=$MY_EDC_TITLE +# List of Connectors to be added on startup +EDC_BROKER_SERVER_KNOWN_CONNECTORS= -# MY_EDC_DESCRIPTION -EDC_IDS_DESCRIPTION=$MY_EDC_DESCRIPTION +# Frequency of refreshing connectors +EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * -# MY_EDC_CURATOR_URL -EDC_IDS_CURATOR=$MY_EDC_CURATOR_URL +# Duration a data offer is shown for offline connectors +EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D -# MY_EDC_CURATOR_NAME -EDC_UI_CURATOR_ORGANIZATION_NAME=$MY_EDC_CURATOR_NAME +# Parallelization for crawling +EDC_BROKER_SERVER_NUM_THREADS=3 -# MY_EDC_MAINTAINER_URL -EDC_IDS_MAINTAINER=$MY_EDC_MAINTAINER_URL -# MY_EDC_MAINTAINER_NAME -EDC_UI_MAINTAINER_ORGANIZATION_NAME=$MY_EDC_MAINTAINER_NAME +# =========================================================== +# Other EDC Config +# =========================================================== # Ports and Paths +MY_EDC_NAME_KEBAB_CASE=broker +MY_EDC_BASE_PATH=/backend +MY_EDC_PROTOCOL=https:// WEB_HTTP_PORT=11001 WEB_HTTP_MANAGEMENT_PORT=11002 WEB_HTTP_PROTOCOL_PORT=11003 @@ -55,19 +46,19 @@ WEB_HTTP_MANAGEMENT_PATH=${MY_EDC_BASE_PATH}/api/v1/management WEB_HTTP_PROTOCOL_PATH=${MY_EDC_BASE_PATH}/api/v1/ids WEB_HTTP_CONTROL_PATH=${MY_EDC_BASE_PATH}/api/v1/control - +EDC_CONNECTOR_NAME=$MY_EDC_NAME_KEBAB_CASE EDC_HOSTNAME=${MY_EDC_FQDN} -EDC_UI_CONNECTOR_ID=${MY_EDC_PROTOCOL}${MY_EDC_FQDN} +# Deprecated IDS Settings +EDC_IDS_ID=urn:connector:$MY_EDC_NAME_KEBAB_CASE +EDC_IDS_TITLE=This will be unavailable starting Core EDC 0.1.0 +EDC_IDS_DESCRIPTION=This will be unavailable starting Core EDC 0.1.0 +EDC_IDS_CURATOR=http://this-will-be-unavailable-starting-core-edc-0-1-0 +EDC_IDS_MAINTAINER=http://this-will-be-unavailable-starting-core-edc-0-1-0 MY_EDC_IDS_BASE_URL=${MY_EDC_PROTOCOL}${MY_EDC_FQDN} IDS_WEBHOOK_ADDRESS=${MY_EDC_IDS_BASE_URL} EDC_IDS_ENDPOINT=${MY_EDC_IDS_BASE_URL}${WEB_HTTP_PROTOCOL_PATH} -# Flyway Extension: Required -MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url -MY_EDC_JDBC_USER=missing-postgresql-user -MY_EDC_JDBC_PASSWORD=missing-postgresql-password - # Flyway Extension: Defaults EDC_DATASOURCE_DEFAULT_NAME=default EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL @@ -104,13 +95,10 @@ EDC_DATASOURCE_DATAPLANEINSTANCE_URL=$MY_EDC_JDBC_URL EDC_DATASOURCE_DATAPLANEINSTANCE_USER=$MY_EDC_JDBC_USER EDC_DATASOURCE_DATAPLANEINSTANCE_PASSWORD=$MY_EDC_JDBC_PASSWORD -# Broker Extension Configuration -POLICY_BROKER_BLACKLIST=REFERRING_CONNECTOR - # Oauth default configurations EDC_OAUTH_PROVIDER_AUDIENCE=idsc:IDS_CONNECTORS_ALL -# This file could contain an entry replacing the EDC_KEYSTORE ENV var -# but for some reason it is required, and EDC won't start up if it isn't configured -# it is created in the Dockerfile +# This file could contain an entry replacing the EDC_KEYSTORE ENV var, +# but for some reason it is required, and EDC won't start up if it isn't configured. +# It will be created in the Dockerfile EDC_VAULT=/emtpy-properties-file.properties diff --git a/connector/src/main/resources/logging.properties b/connector/src/main/resources/logging.properties index b4d12f28f..17dfd8a75 100644 --- a/connector/src/main/resources/logging.properties +++ b/connector/src/main/resources/logging.properties @@ -5,3 +5,4 @@ java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s %6$s%n org.eclipse.dataspaceconnector.level = FINE org.eclipse.dataspaceconnector.handler = java.util.logging.ConsoleHandler +org.eclipse.edc.api.observability.ObservabilityApiController.level = ERROR diff --git a/docker-compose.yaml b/docker-compose.yaml index 34e891584..2f1b29466 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,36 +8,29 @@ services: - EDC_UI_ACTIVE_PROFILE=broker - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:11002/backend/api/v1/management - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue - - EDC_BROKER_SERVER_KNOWN_CONNECTORS=http://connector:11003/api/v1/ids/data broker: image: ${BROKER_IMAGE} depends_on: - broker-postgresql - connector environment: - # Data Management API Key - EDC_API_AUTH_KEY: ApiKeyDefaultValue - - # Deployment Configuration - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" + # Broker Configuration MY_EDC_FQDN: "broker" MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" # Local Dev / Docker-Compose Config MY_EDC_PROTOCOL: "http://" # We don't have TLS in the docker container - MY_EDC_IDS_BASE_URL: "http://broker:11003" # We have no reverse proxy removing ports in the docker compose + MY_EDC_IDS_BASE_URL: "http://broker:11003" # Add the port, because we have no reverse proxy erasing the ports here EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' - - # Update connectors every 20s in dev - EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" - # No parallelism in dev - EDC_BROKER_SERVER_NUM_THREADS: "1" - # Hide offline data offers after 1 minute in dev - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" + EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" # Update connectors every 20s in dev + EDC_BROKER_SERVER_NUM_THREADS: "1" # No parallelism in dev + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" # Hide offline data offers after 1 minute in dev + EDC_API_AUTH_KEY: "ApiKeyDefaultValue" # Management API Key (Access to UI should be secured by other means, as this key is sent to the UI) ports: - '11001:11001' - '11002:11002' @@ -53,7 +46,6 @@ services: POSTGRESQL_DATABASE: edc volumes: - 'broker-postgresql:/bitnami/postgresql' - connector-ui: image: ${EDC_UI_IMAGE} ports: @@ -65,8 +57,6 @@ services: - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue connector: image: ${EDC_CE_IMAGE} - depends_on: - - connector-postgresql environment: MY_EDC_NAME_KEBAB_CASE: "example-connector" MY_EDC_TITLE: "EDC Connector" @@ -79,14 +69,10 @@ services: # Data Management API Key EDC_API_AUTH_KEY: ApiKeyDefaultValue + # Local Dev / Docker-Compose Config MY_EDC_PROTOCOL: "http://" MY_EDC_FQDN: "connector" MY_EDC_IDS_BASE_URL: "http://connector:11003" - - MY_EDC_JDBC_URL: jdbc:postgresql://connector-postgresql:5432/edc - MY_EDC_JDBC_USER: edc - MY_EDC_JDBC_PASSWORD: edc - EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' @@ -98,18 +84,6 @@ services: - '22005:5005' volumes: - ./docs/getting-started/secrets:/secrets - connector-postgresql: - image: docker.io/bitnami/postgresql:15 - restart: always - environment: - POSTGRESQL_USERNAME: edc - POSTGRESQL_PASSWORD: edc - POSTGRESQL_DATABASE: edc - ports: - - '54322:5432' - volumes: - - 'connector-postgresql:/bitnami/postgresql' - volumes: broker-postgresql: driver: local From bb817d43ff9ae846e198f15f05d58cfff17cb691 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 11:32:01 +0200 Subject: [PATCH 047/295] perf: improve json sql queries (#116) --- .../ext/brokerserver/dao/queries/DataOfferQueries.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java index 7b5274f99..eb4bac1bc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java @@ -46,10 +46,10 @@ public List forCatalogPage(DSLContext dsl, String searchQuery, C var d = Tables.DATA_OFFER; // Asset Properties from JSON to be used in sorting / filtering - var assetId = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.ASSET_ID); - var assetTitle = DSL.coalesce(JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.ASSET_NAME), assetId); - var assetDescription = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.DESCRIPTION); - var assetKeywords = JsonbDSL.extractPathText(d.ASSET_PROPERTIES, AssetProperty.KEYWORDS); + var assetId = JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.ASSET_ID); + var assetTitle = DSL.coalesce(JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.ASSET_NAME), assetId); + var assetDescription = JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.DESCRIPTION); + var assetKeywords = JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.KEYWORDS); // This date should always be non-null // It's used in the UI to display the last relevant change date of a connector From 29312eeb33c1e5d9fe509df903b1bd8316a542e5 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 13:31:01 +0200 Subject: [PATCH 048/295] chore: prepare initial release (#117) --- .env | 6 +-- .github/ISSUE_TEMPLATE/release.md | 53 +++++++++++++++++--------- .github/workflows/release_docs_zip.yml | 30 +++++++++++++++ CHANGELOG.md | 8 +++- README.md | 12 +++--- docker-compose.yaml | 2 - gradle.properties | 2 +- 7 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/release_docs_zip.yml diff --git a/.env b/.env index 019299700..9f2b12d55 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:0.0.1 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:3.3.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6 diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 704db8c26..bf05d4d40 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -14,26 +14,41 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Release [edc-ui](https://github.com/sovity/edc-ui), this might require several steps. - [ ] Release [edc-extensions](https://github.com/sovity/edc-extensions), this might require several steps. -- [ ] Update the CHANGELOG.md. - - [ ] Decide a release version depending on major/minor/patch changes. - - [ ] Add a clean `Unreleased` version and rename the old section to the release version. - - [ ] Remove empty sections from the release version's patch notes. - - [ ] Write or review the `Deployment Migration Notes` section. - - [ ] Write or review a release summary. -- [ ] Set the release versions in the [docker-compose's .env file](.env). -- [ ] Set the release versions in the [Deployment Section of our README.md](README.md#deployment). Use text instead of a - link for the broker server version, as the package version does only exist after the release. -- [ ] Commit those changes in a `release-prep` PR. -- [ ] Manually test the release. -- [ ] Create a release and re-use the changelog section as release description. +- [ ] Decide a release version depending on major/minor/patch changes in the CHANGELOG.md. +- [ ] Update this issue's title to the new version +- [ ] `release-prep` PR: + - [ ] Update the CHANGELOG.md. + - [ ] Add a clean `Unreleased` version. + - [ ] Add the version to the old section. + - [ ] Add the current date to the old version. + - [ ] Write or review the `Deployment Migration Notes` section. + - [ ] Ensure the `Deployment Migration Notes` contains the compatible docker images. + - [ ] Write or review a release summary. + - [ ] Remove empty sections from the patch notes. + - [ ] Update + the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to + contain the released edc-extensions version. + - [ ] Set the broker server release version in + the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). + - [ ] Set the edc ui release version in + the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). + - [ ] Merge the `release-prep` PR. +- [ ] Wait for the main branch to be green. +- [ ] Test the `docker-compose.yaml`. +- [ ] Create a release and re-use the changelog section as release description, and the version as title. - [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). -- [ ] Revert the versions in the [docker-compose's .env file](.env) back to the snapshot/nightly versions of the - components. -- [ ] Change the broker server release version references in - the [Deployment Section of our README.md](README.md#deployment) to links to the package version. -- [ ] Change the broker server release version references in the Release Description to links to the package version. +- [ ] Checkout the release tag and check test the `docker-compose.yaml`. +- [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. +- [ ] `release-cleanup` PR: + - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest/main. + - [ ] Change the broker server release version references in + the [Deployment Section of our README.md](https://github.com/sovity/edc-broker-server-extension/blob/main/README.md#deployment) + to links to the package version. + - [ ] Change the broker server release version references in the Release Description to links to the package + version. + - [ ] Add the UI - [ ] Commit those changes in a `release-cleanup` PR. - [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md]. Apply changes where it makes sense. -- [ ] Notify the deployment team with the release notes, which should now contain both product changes and a - configuration migration guide. \ No newline at end of file +- [ ] Notify the deployment team with Deployment Docs Zip file attached to the release, which should now contain both + product changes and a deployment migration guide. \ No newline at end of file diff --git a/.github/workflows/release_docs_zip.yml b/.github/workflows/release_docs_zip.yml new file mode 100644 index 000000000..3dc91eadb --- /dev/null +++ b/.github/workflows/release_docs_zip.yml @@ -0,0 +1,30 @@ +name: Release Docs Zip File + +on: + release: + types: [ published ] + +env: + IMAGE_NAME_BASE: ${{ github.repository_owner }} + +jobs: + add_docs_zip_to_release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Archive deployment-relevant documentation. + run: | + ARCHIVE_FILE_NAME="broker-server-docs-release-${GITHUB_REF#refs/tags/v}.zip" + echo "ARCHIVE_FILE_NAME=$ARCHIVE_FILE_NAME" >> $GITHUB_ENV + - name: Archive deployment-relevant documentation + uses: vimtor/action-zip@v1 + with: + files: README.md CHANGELOG.md docker-compose.yaml .env connector/.env connector/Dockerfile connector/README.md + dest: ${{ env.ARCHIVE_FILE_NAME }} + - name: Upload deployment-relevant documentation + uses: xresloader/upload-to-github-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + file: ${{ env.ARCHIVE_FILE_NAME }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6355f1e..b7faf41a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,13 @@ Initial Broker PoC Release with a minimalistic feature set. ### Deployment Migration Notes -Please view the [README.md](README.md#deployment) for initial deployment instructions. +Please view the [Deployment Section in the README.md](README.md#deployment) for initial deployment instructions. + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.0.1` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6` +- Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) ### Major diff --git a/README.md b/README.md index 94c15923b..b4ce3384f 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,12 @@ Create an issue using the [release template](.github/ISSUE_TEMPLATE/release.md) ### Deployment Units -| Deployment Unit | Version / Details | -|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | -| Postgresql | 15 or compatible version | -| Broker Backend | broker-server-ce:0.0.1 | -| Broker UI | [edc-ui:0.0.1-milestone-8-sovity5](https://github.com/sovity/edc-ui/pkgs/container/edc-ui/91758285?tag=0.0.1-milestone-8-sovity5) | +| Deployment Unit | Version / Details | +|----------------------------------------------------------------|-----------------------------------------------------------------------------| +| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | +| Postgresql | 15 or compatible version | +| Broker Backend | broker-server-ce, see [CHANGELOG.md](CHANGELOG.md) for compatible versions. | +| Broker UI | edc-ui, see [CHANGELOG.md](CHANGELOG.md) for compatible versions. | ### Configuration diff --git a/docker-compose.yaml b/docker-compose.yaml index 2f1b29466..702a9a6dc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -82,8 +82,6 @@ services: - '22003:11003' - '22004:11004' - '22005:5005' - volumes: - - ./docs/getting-started/secrets:/secrets volumes: broker-postgresql: driver: local diff --git a/gradle.properties b/gradle.properties index cce85f653..3e92b4516 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=3.3.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From cedec49d5b97e61d192588ca38e2ec575d8f3511 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 13:57:05 +0200 Subject: [PATCH 049/295] chore: try fix release pipeline (#118) --- .github/ISSUE_TEMPLATE/release.md | 2 +- .github/workflows/release_docs_zip.yml | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index bf05d4d40..ed4c997c3 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -34,7 +34,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. -- [ ] Test the `docker-compose.yaml`. +- [ ] Test the `docker-compose.yaml` with `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main`. - [ ] Create a release and re-use the changelog section as release description, and the version as title. - [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). - [ ] Checkout the release tag and check test the `docker-compose.yaml`. diff --git a/.github/workflows/release_docs_zip.yml b/.github/workflows/release_docs_zip.yml index 3dc91eadb..159418958 100644 --- a/.github/workflows/release_docs_zip.yml +++ b/.github/workflows/release_docs_zip.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: Archive deployment-relevant documentation. run: | - ARCHIVE_FILE_NAME="broker-server-docs-release-${GITHUB_REF#refs/tags/v}.zip" + ARCHIVE_FILE_NAME="broker-server-release-${GITHUB_REF#refs/tags/v}-deployment-docs.zip" echo "ARCHIVE_FILE_NAME=$ARCHIVE_FILE_NAME" >> $GITHUB_ENV - name: Archive deployment-relevant documentation uses: vimtor/action-zip@v1 @@ -23,8 +23,9 @@ jobs: files: README.md CHANGELOG.md docker-compose.yaml .env connector/.env connector/Dockerfile connector/README.md dest: ${{ env.ARCHIVE_FILE_NAME }} - name: Upload deployment-relevant documentation - uses: xresloader/upload-to-github-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: svenstaro/upload-release-action@v2 with: + repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ env.ARCHIVE_FILE_NAME }} + tag: ${{ github.ref }} + overwrite: true From 95fd566de953ae79a456e209cfbd8c9d20c861c1 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 14:04:54 +0200 Subject: [PATCH 050/295] chore: try fix release pipeline (2) (#119) --- .github/workflows/release_docs_zip.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release_docs_zip.yml b/.github/workflows/release_docs_zip.yml index 159418958..c478d1c64 100644 --- a/.github/workflows/release_docs_zip.yml +++ b/.github/workflows/release_docs_zip.yml @@ -17,6 +17,7 @@ jobs: run: | ARCHIVE_FILE_NAME="broker-server-release-${GITHUB_REF#refs/tags/v}-deployment-docs.zip" echo "ARCHIVE_FILE_NAME=$ARCHIVE_FILE_NAME" >> $GITHUB_ENV + zip -r -q $ARCHIVE_FILE_NAME README.md CHANGELOG.md docker-compose.yaml .env connector/.env connector/Dockerfile connector/README.md - name: Archive deployment-relevant documentation uses: vimtor/action-zip@v1 with: From 0ed8e0bae321c7617eb462ffade769a9c7de4f8f Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 14:07:34 +0200 Subject: [PATCH 051/295] chore: try fix release pipeline (3) --- .github/workflows/release_docs_zip.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/release_docs_zip.yml b/.github/workflows/release_docs_zip.yml index c478d1c64..844f382b9 100644 --- a/.github/workflows/release_docs_zip.yml +++ b/.github/workflows/release_docs_zip.yml @@ -18,11 +18,6 @@ jobs: ARCHIVE_FILE_NAME="broker-server-release-${GITHUB_REF#refs/tags/v}-deployment-docs.zip" echo "ARCHIVE_FILE_NAME=$ARCHIVE_FILE_NAME" >> $GITHUB_ENV zip -r -q $ARCHIVE_FILE_NAME README.md CHANGELOG.md docker-compose.yaml .env connector/.env connector/Dockerfile connector/README.md - - name: Archive deployment-relevant documentation - uses: vimtor/action-zip@v1 - with: - files: README.md CHANGELOG.md docker-compose.yaml .env connector/.env connector/Dockerfile connector/README.md - dest: ${{ env.ARCHIVE_FILE_NAME }} - name: Upload deployment-relevant documentation uses: svenstaro/upload-release-action@v2 with: From a880efa316015f90da7aed180a9b983680374d49 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 14:19:25 +0200 Subject: [PATCH 052/295] Update README.md --- connector/README.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/connector/README.md b/connector/README.md index 74644f02b..ee76b6805 100644 --- a/connector/README.md +++ b/connector/README.md @@ -14,25 +14,21 @@

-## Broker Server Image +## Image Variants -The Broker Server Extension together with other EDC Extensions are built into Docker Images. +The Broker Server is built in differnt variants: -## Different Image Types - -Our EDC Community Edition builds several docker images in different configurations. - -| Docker Image | Type | Purpose | Features | -|-------------------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [broker-server-dev](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-dev) | Development |
  • Lightweight local development
  • Used in EDC UI's Getting Started section
|
  • IDS Broker Server Extension(s)
  • Management API Auth via API Keys
  • Mock IAM
| -| [broker-server-ce](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-ce) | Community Edition |
  • Deploy the Broker Server
|
  • IDS Broker Server Extension(s)
  • Management API Auth via API Keys
  • DAPS Authentication
  • PostgreSQL Persistence & Flyway
| +| Docker Image | Type | Purpose | Features | +|-------------------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| [broker-server-dev](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-dev) | Development |
  • Local Deployment via our `docker-compose.yaml`
  • E2E Testing
|
  • Broker Server Extension(s)
  • PostgreSQL Persistence & Flyway
  • Mock IAM
| +| [broker-server-ce](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-ce) | Community Edition |
  • Productive Deployment
|
  • Broker Server Extension(s)
  • PostgreSQL Persistence & Flyway
  • DAPS Authentication
| ## Image Tags -| Tag | Description | -|---------|-----------------------------------| -| latest | latest version of our main branch | -| release | latest release of this repository | +| Tag | Description | +|---------------|-----------------------------------| +| latest / main | latest version of our main branch | +| release | latest release of this repository | ## License From af1eb6c8c2e9164e0fa271443f0339ecf6a8038a Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 6 Jun 2023 15:24:17 +0200 Subject: [PATCH 053/295] chore: post-release cleanup (#120) --- .env | 6 +++--- .github/ISSUE_TEMPLATE/release.md | 30 +++++++++++------------------- gradle.properties | 2 +- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/.env b/.env index 9f2b12d55..5e0e82601 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:0.0.1 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:3.3.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index ed4c997c3..19ead817d 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -25,13 +25,10 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Ensure the `Deployment Migration Notes` contains the compatible docker images. - [ ] Write or review a release summary. - [ ] Remove empty sections from the patch notes. - - [ ] Update - the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to - contain the released edc-extensions version. - - [ ] Set the broker server release version in - the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). - - [ ] Set the edc ui release version in - the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). + - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the released edc-extensions version. + - [ ] Set the broker server release version in the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). + - [ ] Set the EDC UI release version in the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). + - [ ] Set the EDC CE release version in the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. - [ ] Test the `docker-compose.yaml` with `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main`. @@ -39,16 +36,11 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). - [ ] Checkout the release tag and check test the `docker-compose.yaml`. - [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. +- [ ] Notify the deployment team with Deployment Docs Zip file attached to the release, which should now contain both product changes and a deployment migration guide. - [ ] `release-cleanup` PR: - - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest/main. - - [ ] Change the broker server release version references in - the [Deployment Section of our README.md](https://github.com/sovity/edc-broker-server-extension/blob/main/README.md#deployment) - to links to the package version. - - [ ] Change the broker server release version references in the Release Description to links to the package - version. - - [ ] Add the UI -- [ ] Commit those changes in a `release-cleanup` PR. -- [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md]. Apply changes where it - makes sense. -- [ ] Notify the deployment team with Deployment Docs Zip file attached to the release, which should now contain both - product changes and a deployment migration guide. \ No newline at end of file + - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the EDC UI. + - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the EDC CE. + - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the Broker Server. + - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the edc-extensions version `0.0.1-SNAPSHOT`. + - [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-broker-server-extension/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. + - [ ] Merge the `release-cleanup` PR. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3e92b4516..cce85f653 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions -sovityEdcExtensionsVersion=3.3.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 1c3efb43b9c73895f657c2e1d23aa9a5177fe60e Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 7 Jun 2023 08:52:04 +0200 Subject: [PATCH 054/295] feat: max data offers per connector and contract offers per data offer (#98) * feat: max data offers per connector * feat: max contract offers per connector * refactor: DataOfferFetcher * refactor: DataOfferFetcher * refactor: minor remarks * feat: DataOfferLimitsEnforcer * feat: DataOfferLimitsEnforcer * feat: DataOfferLimitsEnforcer * refactor: further refactorings * refactor: further refactorings * refactor: further refactorings * test: add DataOfferLimitsEnforcerTest * test: add DataOfferLimitsEnforcerTest * test: add DataOfferLimitsEnforcerTest * test: add DataOfferLimitsEnforcerTest * test: add DataOfferLimitsEnforcerTest * refactor: DataOfferLimitsEnforcer * test: no_limit_and_two_dataofffers_and_contractoffer_should_not_limit --- connector/.env | 2 + .../V2__Broker_Server_Initial_DB_Model.sql | 21 +- .../brokerserver/BrokerServerExtension.java | 6 + .../BrokerServerExtensionContextBuilder.java | 4 +- .../services/ConnectorCreator.java | 4 + .../services/logging/BrokerEventLogger.java | 40 ++++ .../ConnectorUpdateSuccessWriter.java | 9 +- .../offers/DataOfferLimitsEnforcer.java | 90 ++++++++ .../services/api/CatalogApiTest.java | 4 + .../offers/DataOfferLimitsEnforcerTest.java | 204 ++++++++++++++++++ 10 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java diff --git a/connector/.env b/connector/.env index 35a81de91..365149db4 100644 --- a/connector/.env +++ b/connector/.env @@ -27,6 +27,8 @@ EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D # Parallelization for crawling EDC_BROKER_SERVER_NUM_THREADS=3 +EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR=50 +EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_CONNECTOR=10 # =========================================================== diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index 45481e629..55fe17e17 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -1,6 +1,8 @@ create type connector_online_status as enum ('ONLINE', 'OFFLINE'); create type measurement_type as enum ('CONNECTOR_REFRESH'); create type measurement_error_status as enum ('ERROR', 'OK'); +create type connector_data_offers_exceeded as enum ('OK', 'EXCEEDED'); +create type connector_contract_offers_exceeded as enum ('OK', 'EXCEEDED'); create table connector ( @@ -10,6 +12,8 @@ create table connector last_refresh_attempt_at timestamp with time zone, last_successful_refresh_at timestamp with time zone, online_status connector_online_status not null, + data_offers_exceeded connector_data_offers_exceeded not null, + contract_offers_exceeded connector_contract_offers_exceeded not null, PRIMARY KEY (endpoint) ); @@ -57,7 +61,19 @@ create type broker_event_type as enum ( 'CONTRACT_OFFER_UPDATED', --Contract Offer was clicked - 'CONTRACT_OFFER_CLICK' + 'CONTRACT_OFFER_CLICK', + + --Connector Data Offer Limit was exceeded + 'CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED', + + --Connector Data Offer Limit was not exceeded + 'CONNECTOR_DATA_OFFER_LIMIT_OK', + + --Connector Contract Offer Limit was exceeded + 'CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED', + + --Connector Contract Offer Limit was not exceeded + 'CONNECTOR_CONTRACT_OFFER_LIMIT_OK' ); create type broker_event_status as enum ( @@ -77,8 +93,7 @@ create table broker_event_log event_status broker_event_status not null, connector_endpoint text, asset_id text, - error_stack text, - duration_in_ms bigint + error_stack text ); create table broker_execution_time_measurement diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index ed8741d93..7355790b8 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -39,6 +39,12 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String HIDE_OFFLINE_DATA_OFFERS_AFTER = "edc.broker.server.hide.offline.data.offers.after"; + @Setting + public static final String MAX_DATA_OFFERS_PER_CONNECTOR = "edc.broker.server.max.data.offers.per.connector"; + + @Setting + public static final String MAX_CONTRACT_OFFERS_PER_CONNECTOR = "edc.broker.server.max.contract.offers.per.connector"; + @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index a29efae70..37ea841fb 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -40,6 +40,7 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferRecordUpdater; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferBuilder; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchApplier; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchBuilder; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; @@ -91,6 +92,7 @@ public static BrokerServerExtensionContext buildContext( var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); var dataOfferRecordUpdater = new DataOfferRecordUpdater(); var dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); + var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(config, brokerEventLogger); var dataOfferPatchBuilder = new DataOfferPatchBuilder( dataOfferContractOfferQueries, dataOfferQueries, @@ -99,7 +101,7 @@ public static BrokerServerExtensionContext buildContext( ); var dataOfferPatchApplier = new DataOfferPatchApplier(); var dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); - var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger, dataOfferWriter); + var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger, dataOfferWriter, dataOfferLimitsEnforcer); var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger, monitor); var contractOfferFetcher = new ContractOfferFetcher(catalogService); var fetchedDataOfferBuilder = new DataOfferBuilder(objectMapper); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index 72f841555..2bb4323d0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -15,6 +15,8 @@ package de.sovity.edc.ext.brokerserver.services; import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; @@ -56,6 +58,8 @@ private ConnectorRecord newConnectorRow(String endpoint) { connector.setConnectorId(UrlUtils.getEverythingBeforeThePath(endpoint)); connector.setCreatedAt(OffsetDateTime.now()); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); return connector; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index d8b3abd59..83e32e7a1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -71,6 +71,46 @@ public void logConnectorUpdateStatusChange(DSLContext dsl, String connectorEndpo logEntry.insert(); } + public void logConnectorUpdateDataOfferLimitExceeded(Integer maxDataOffersPerConnector, String endpoint) { + var logEntry = new BrokerEventLogRecord(); + logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(endpoint); + logEntry.setUserMessage("Connector has exceeded the maximum number of data offers: " + maxDataOffersPerConnector); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.insert(); + } + + public void logConnectorUpdateDataOfferLimitOk(Integer maxDataOffersPerConnector, String endpoint) { + var logEntry = new BrokerEventLogRecord(); + logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_OK); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(endpoint); + logEntry.setUserMessage("Connector is not exceeding maximum number of data offers limits anymore: " + maxDataOffersPerConnector); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.insert(); + } + + public void logConnectorUpdateContractOfferLimitExceeded(Integer maxContractOffersPerConnector, String endpoint) { + var logEntry = new BrokerEventLogRecord(); + logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(endpoint); + logEntry.setUserMessage("Connector has exceeded maximum number of contract offers per data offer limit: " + maxContractOffersPerConnector); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.insert(); + } + + public void logConnectorUpdateContractOfferLimitOk(Integer maxContractOffersPerConnector, String endpoint) { + var logEntry = new BrokerEventLogRecord(); + logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_OK); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(endpoint); + logEntry.setUserMessage("Connector is not exceeding maximum number of contract offers per data offer limits anymore: " + maxContractOffersPerConnector); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.insert(); + } + private BrokerEventLogRecord logEntry(DSLContext dsl, BrokerEventType eventType, String connectorEndpoint, String userMessage) { var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEventStatus(BrokerEventStatus.OK); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 9a93008cc..9af4f7a20 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; import lombok.RequiredArgsConstructor; @@ -30,6 +31,7 @@ public class ConnectorUpdateSuccessWriter { private final BrokerEventLogger brokerEventLogger; private final DataOfferWriter dataOfferWriter; + private final DataOfferLimitsEnforcer dataOfferLimitsEnforcer; public void handleConnectorOnline( DSLContext dsl, @@ -38,6 +40,10 @@ public void handleConnectorOnline( ) { var now = OffsetDateTime.now(); + // Limit data offers and log limitation if necessary + var limitedDataOffers = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, limitedDataOffers); + // Log Status Change and set status to online if necessary if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE || connector.getLastRefreshAttemptAt() == null) { brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.ONLINE); @@ -56,7 +62,6 @@ public void handleConnectorOnline( } // Update data offers - dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), dataOffers, changes); + dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), limitedDataOffers.abbreviatedDataOffers(), changes); } - } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java new file mode 100644 index 000000000..3ee5a1e51 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.util.Collection; + +@RequiredArgsConstructor +public class DataOfferLimitsEnforcer { + private final Config config; + private final BrokerEventLogger brokerEventLogger; + + public record DataOfferLimitsEnforced( + Collection abbreviatedDataOffers, + boolean dataOfferLimitsExceeded, + boolean contractOfferLimitsExceeded + ) { + } + + public DataOfferLimitsEnforced enforceLimits(Collection dataOffers) { + // Get limits from config + var maxDataOffers = config.getInteger(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, -1); + var maxContractOffers = config.getInteger(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR, -1); + var offerList = dataOffers.stream().toList(); + + // No limits set + if (maxDataOffers == -1 && maxContractOffers == -1) { + return new DataOfferLimitsEnforced(dataOffers, false, false); + } + + // Validate if limits exceeded + var dataOfferLimitsExceeded = false; + if (maxDataOffers != -1 && offerList.size() > maxDataOffers) { + offerList = offerList.subList(0, maxDataOffers); + dataOfferLimitsExceeded = true; + } + + var contractOfferLimitsExceeded = false; + for (var dataOffer : offerList) { + var contractOffers = dataOffer.getContractOffers(); + if (contractOffers != null && maxContractOffers != -1 && contractOffers.size() > maxContractOffers) { + dataOffer.setContractOffers(contractOffers.subList(0, maxContractOffers)); + contractOfferLimitsExceeded = true; + } + } + + // Create new list with limited offers + return new DataOfferLimitsEnforced(offerList, dataOfferLimitsExceeded, contractOfferLimitsExceeded); + } + + public void logEnforcedLimitsIfChanged(ConnectorRecord connector, DataOfferLimitsEnforced enforcedLimits) { + // DataOffer + if (enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.OK) { + brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.EXCEEDED); + } else if (!enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.EXCEEDED) { + brokerEventLogger.logConnectorUpdateDataOfferLimitOk(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + } + + // ContractOffer + if (enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.OK) { + brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.EXCEEDED); + } else if (!enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.EXCEEDED) { + brokerEventLogger.logConnectorUpdateContractOfferLimitOk(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + } + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 5e86fff55..f91b663a5 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -19,6 +19,8 @@ import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -62,6 +64,8 @@ void testQueryConnectors() { connector.setCreatedAt(today.minusDays(1)); connector.setLastRefreshAttemptAt(today); connector.setLastSuccessfulRefreshAt(today); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); connector.insert(); var dataOffer = dsl.newRecord(Tables.DATA_OFFER); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java new file mode 100644 index 000000000..fee32fbeb --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java @@ -0,0 +1,204 @@ +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import org.eclipse.edc.spi.system.configuration.Config; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DataOfferLimitsEnforcerTest { + DataOfferLimitsEnforcer dataOfferLimitsEnforcer; + Config config; + BrokerEventLogger brokerEventLogger; + + @BeforeEach + void setup() { + config = mock(Config.class); + brokerEventLogger = mock(BrokerEventLogger.class); + dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(config, brokerEventLogger); + } + + @Test + void no_limit_and_two_dataofffers_and_contractoffer_should_not_limit() { + // arrange + int maxDataOffers = -1; + int maxContractOffers = -1; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var myDataOffer = new FetchedDataOffer(); + myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + var dataOffers = List.of(myDataOffer, myDataOffer); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + var actual = enforcedLimits.abbreviatedDataOffers(); + var contractOffersLimitExceeded = enforcedLimits.contractOfferLimitsExceeded(); + var dataOffersLimitExceeded = enforcedLimits.dataOfferLimitsExceeded(); + + // assert + assertThat(actual).hasSize(2); + assertFalse(contractOffersLimitExceeded); + assertFalse(dataOffersLimitExceeded); + } + + @Test + void limit_zero_and_one_dataoffers_should_result_to_none() { + // arrange + int maxDataOffers = 0; + int maxContractOffers = 0; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var dataOffers = List.of(new FetchedDataOffer()); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + var actual = new ArrayList(enforcedLimits.abbreviatedDataOffers()); + var contractOffersLimitExceeded = enforcedLimits.contractOfferLimitsExceeded(); + var dataOffersLimitExceeded = enforcedLimits.dataOfferLimitsExceeded(); + + // assert + assertThat(actual).isEmpty(); + assertFalse(contractOffersLimitExceeded); + assertTrue(dataOffersLimitExceeded); + } + + @Test + void limit_one_and_two_dataoffers_should_result_to_one() { + // arrange + int maxDataOffers = 1; + int maxContractOffers = 1; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var myDataOffer = new FetchedDataOffer(); + myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + var dataOffers = List.of(myDataOffer, myDataOffer); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + var actual = new ArrayList(enforcedLimits.abbreviatedDataOffers()); + var contractOffersLimitExceeded = enforcedLimits.contractOfferLimitsExceeded(); + var dataOffersLimitExceeded = enforcedLimits.dataOfferLimitsExceeded(); + + // assert + assertThat(actual).hasSize(1); + assertThat(((FetchedDataOffer) actual.get(0)).getContractOffers()).hasSize(1); + assertTrue(contractOffersLimitExceeded); + assertTrue(dataOffersLimitExceeded); + } + + @Test + void verify_logConnectorUpdateDataOfferLimitExceeded() { + // arrange + var connector = new ConnectorRecord(); + connector.setEndpoint("http://localhost:8080"); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + + int maxDataOffers = 1; + int maxContractOffers = 1; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var myDataOffer = new FetchedDataOffer(); + myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + var dataOffers = List.of(myDataOffer, myDataOffer); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + + // assert + verify(brokerEventLogger).logConnectorUpdateDataOfferLimitExceeded(1, connector.getEndpoint()); + } + + @Test + void verify_logConnectorUpdateDataOfferLimitOk() { + // arrange + var connector = new ConnectorRecord(); + connector.setEndpoint("http://localhost:8080"); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.EXCEEDED); + + int maxDataOffers = -1; + int maxContractOffers = -1; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var myDataOffer = new FetchedDataOffer(); + myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + var dataOffers = List.of(myDataOffer, myDataOffer); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + + // assert + verify(brokerEventLogger).logConnectorUpdateDataOfferLimitOk(2, connector.getEndpoint()); + } + + @Test + void verify_logConnectorUpdateContractOfferLimitExceeded() { + // arrange + var connector = new ConnectorRecord(); + connector.setEndpoint("http://localhost:8080"); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + + int maxDataOffers = 1; + int maxContractOffers = 1; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var myDataOffer = new FetchedDataOffer(); + myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + var dataOffers = List.of(myDataOffer, myDataOffer); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + + // assert + verify(brokerEventLogger).logConnectorUpdateContractOfferLimitExceeded(1, connector.getEndpoint()); + } + + @Test + void verify_logConnectorUpdateContractOfferLimitOk() { + // arrange + var connector = new ConnectorRecord(); + connector.setEndpoint("http://localhost:8080"); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.EXCEEDED); + + int maxDataOffers = -1; + int maxContractOffers = -1; + when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + + var myDataOffer = new FetchedDataOffer(); + myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + var dataOffers = List.of(myDataOffer, myDataOffer); + + // act + var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + + // assert + verify(brokerEventLogger).logConnectorUpdateContractOfferLimitOk(2, connector.getEndpoint()); + } +} From dcf2b6db1f8cb496eb346997f8fcdf5d14bff0bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:09:13 +0200 Subject: [PATCH 055/295] chore(deps): bump org.flywaydb.flyway from 9.19.1 to 9.19.3 (#123) Bumps org.flywaydb.flyway from 9.19.1 to 9.19.3. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index fb294bd60..703264fcd 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -26,7 +26,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.19.1" + id("org.flywaydb.flyway") version "9.19.3" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From 2e2c584803c9ee5410384676070b055940c21763 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 07:27:04 +0200 Subject: [PATCH 056/295] chore(deps): bump org.flywaydb.flyway from 9.19.3 to 9.19.4 (#126) Bumps org.flywaydb.flyway from 9.19.3 to 9.19.4. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 703264fcd..f8ae7639b 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -26,7 +26,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.19.3" + id("org.flywaydb.flyway") version "9.19.4" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From 720d26eeb46734810395449255ae35de955b957c Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 13 Jun 2023 07:43:07 +0200 Subject: [PATCH 057/295] feat: filtering of data offers (#125) * feat: filtering of data offers * chore: reformat code and organize imports * chore: fix checkstyle --- .../workflows/add_pullrequest_to_project.yml | 6 - .../V2__Broker_Server_Initial_DB_Model.sql | 12 +- .../BrokerServerExtensionContextBuilder.java | 35 +++- .../ext/brokerserver/dao/AssetProperty.java | 17 ++ .../brokerserver/dao/ConnectorQueries.java | 43 ++++ .../DataOfferContractOfferQueries.java | 2 +- .../brokerserver/dao/DataOfferQueries.java | 32 +++ .../CatalogQueryAvailableFilterFetcher.java | 57 ++++++ .../CatalogQueryContractOfferFetcher.java | 52 +++++ .../catalog/CatalogQueryDataOfferFetcher.java | 62 ++++++ .../dao/pages/catalog/CatalogQueryFields.java | 66 ++++++ .../catalog/CatalogQueryFilterService.java | 65 ++++++ .../pages/catalog/CatalogQueryService.java | 60 ++++++ .../catalog/CatalogQuerySortingService.java | 49 +++++ .../models/AvailableFilterValuesQuery.java | 30 +++ .../pages/catalog/models/CatalogPageRs.java | 30 +++ .../catalog/models/CatalogQueryFilter.java | 24 +++ .../CatalogQuerySelectedFilterQuery.java | 30 +++ .../catalog/models/ContractOfferRs.java} | 4 +- .../catalog/models/DataOfferRs.java} | 6 +- .../connector/ConnectorPageQueryService.java} | 39 +--- .../connector/model/ConnectorRs.java} | 4 +- .../dao/queries/DataOfferQueries.java | 141 ------------- .../dao/utils/JsonDeserializationUtils.java | 39 ++++ .../dao/{queries => }/utils/JsonbUtils.java | 2 +- .../dao/{queries => }/utils/LikeUtils.java | 2 +- .../brokerserver/dao/utils/MultisetUtils.java | 30 +++ .../{queries => }/utils/PostgresqlUtils.java | 2 +- .../dao/{queries => }/utils/SearchUtils.java | 2 +- .../services/ConnectorCreator.java | 2 +- .../services/api/CatalogApiService.java | 65 +++--- .../services/api/ConnectorApiService.java | 34 ++-- .../api/filtering/AttributeFilterQuery.java | 34 ++++ .../CatalogFilterAttributeDefinition.java | 33 +++ ...talogFilterAttributeDefinitionService.java | 38 ++++ .../api/filtering/CatalogFilterService.java | 143 +++++++++++++ .../services/queue/ConnectorQueueFiller.java | 2 +- .../services/refreshing/ConnectorUpdater.java | 2 +- .../offers/ContractOfferRecordUpdater.java | 2 +- .../offers/DataOfferPatchBuilder.java | 4 +- .../offers/DataOfferRecordUpdater.java | 2 +- .../brokerserver/utils/CollectionUtils2.java | 4 + .../{queries => }/utils/LikeUtilsTest.java | 2 +- .../ext/brokerserver/db/FlywayTestUtils.java | 4 +- .../services/api/CatalogApiTest.java | 192 +++++++++++++++--- .../offers/DataOfferLimitsEnforcerTest.java | 16 +- .../offers/DataOfferWriterTestDataHelper.java | 2 +- .../offers/DataOfferWriterTestDydi.java | 6 +- 48 files changed, 1236 insertions(+), 294 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{queries => }/DataOfferContractOfferQueries.java (94%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{models/DataOfferContractOfferDbRow.java => pages/catalog/models/ContractOfferRs.java} (87%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{models/DataOfferDbRow.java => pages/catalog/models/DataOfferRs.java} (87%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{queries/ConnectorQueries.java => pages/connector/ConnectorPageQueryService.java} (56%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{models/ConnectorPageDbRow.java => pages/connector/model/ConnectorRs.java} (90%) delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{queries => }/utils/JsonbUtils.java (93%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{queries => }/utils/LikeUtils.java (96%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{queries => }/utils/PostgresqlUtils.java (94%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{queries => }/utils/SearchUtils.java (96%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java rename extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/{queries => }/utils/LikeUtilsTest.java (91%) diff --git a/.github/workflows/add_pullrequest_to_project.yml b/.github/workflows/add_pullrequest_to_project.yml index f80f2d5db..81a5a26f1 100644 --- a/.github/workflows/add_pullrequest_to_project.yml +++ b/.github/workflows/add_pullrequest_to_project.yml @@ -8,12 +8,6 @@ jobs: name: add_pullrequest_to_project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/sovity/projects/9 - github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} - labeled: area/dependency - label-operator: NOT - uses: actions/add-to-project@v0.5.0 with: project-url: https://github.com/orgs/sovity/projects/21 diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql index 55fe17e17..fcf8ee3e5 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql @@ -6,13 +6,13 @@ create type connector_contract_offers_exceeded as enum ('OK', 'EXCEEDED'); create table connector ( - endpoint text not null, - connector_id text not null, - created_at timestamp with time zone not null, + endpoint text not null, + connector_id text not null, + created_at timestamp with time zone not null, last_refresh_attempt_at timestamp with time zone, last_successful_refresh_at timestamp with time zone, - online_status connector_online_status not null, - data_offers_exceeded connector_data_offers_exceeded not null, + online_status connector_online_status not null, + data_offers_exceeded connector_data_offers_exceeded not null, contract_offers_exceeded connector_contract_offers_exceeded not null, PRIMARY KEY (endpoint) @@ -39,7 +39,7 @@ create table data_offer_contract_offer created_at timestamp with time zone not null, updated_at timestamp with time zone, - PRIMARY KEY (contract_offer_id), + PRIMARY KEY (connector_endpoint, asset_id, contract_offer_id), FOREIGN KEY (connector_endpoint, asset_id) REFERENCES data_offer (connector_endpoint, asset_id), FOREIGN KEY (connector_endpoint) REFERENCES connector (endpoint) ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 37ea841fb..ff9f24ab5 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -14,9 +14,16 @@ package de.sovity.edc.ext.brokerserver; -import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferContractOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryAvailableFilterFetcher; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryContractOfferFetcher; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryDataOfferFetcher; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFilterService; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQuerySortingService; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; @@ -28,6 +35,8 @@ import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; +import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterAttributeDefinitionService; +import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; @@ -59,7 +68,7 @@ /** - * Manual Dependency Injection. + * Manual Dependency Injection (DYDI). *

* We want to develop as Java Backend Development is done, but we have * no CDI / DI Framework to rely on. @@ -78,11 +87,18 @@ public static BrokerServerExtensionContext buildContext( var brokerServerSettings = new BrokerServerSettings(config); // Dao - var dataOfferQueries = new DataOfferQueries(brokerServerSettings); + var dataOfferQueries = new DataOfferQueries(); var dataSourceFactory = new DataSourceFactory(config); var dataSource = dataSourceFactory.newDataSource(); var dslContextFactory = new DslContextFactory(dataSource); var connectorQueries = new ConnectorQueries(); + var catalogQuerySortingService = new CatalogQuerySortingService(); + var catalogQueryFilterService = new CatalogQueryFilterService(brokerServerSettings); + var catalogQueryContractOfferFetcher = new CatalogQueryContractOfferFetcher(); + var catalogQueryDataOfferFetcher = new CatalogQueryDataOfferFetcher(catalogQuerySortingService, catalogQueryFilterService, catalogQueryContractOfferFetcher); + var catalogQueryAvailableFilterFetcher = new CatalogQueryAvailableFilterFetcher(catalogQueryFilterService); + var catalogQueryService = new CatalogQueryService(catalogQueryDataOfferFetcher, catalogQueryAvailableFilterFetcher); + var connectorPageQueryService = new ConnectorPageQueryService(); // Services @@ -123,6 +139,8 @@ public static BrokerServerExtensionContext buildContext( var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); var connectorCreator = new ConnectorCreator(connectorQueries); var knownConnectorsInitializer = new KnownConnectorsInitializer(config, connectorQueue, connectorCreator); + var catalogFilterAttributeDefinitionService = new CatalogFilterAttributeDefinitionService(); + var catalogFilterService = new CatalogFilterService(catalogFilterAttributeDefinitionService); // Schedules List> jobs = List.of( @@ -140,12 +158,13 @@ public static BrokerServerExtensionContext buildContext( // UI Capabilities var catalogApiService = new CatalogApiService( paginationMetadataUtils, - dataOfferQueries, + catalogQueryService, policyDtoBuilder, - assetPropertyParser + assetPropertyParser, + catalogFilterService ); var connectorApiService = new ConnectorApiService( - connectorQueries, + connectorPageQueryService, paginationMetadataUtils ); var brokerServerResource = new BrokerServerResourceImpl( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java index e66740284..9d24f3bf2 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java @@ -24,4 +24,21 @@ public class AssetProperty { public static final String ASSET_NAME = "asset:prop:name"; public static final String DESCRIPTION = "asset:prop:description"; public static final String KEYWORDS = "asset:prop:keywords"; + + + public static final String CONTENT_TYPE = "asset:prop:contenttype"; + public static final String ORIGINATOR = "asset:prop:originator"; + public static final String ORIGINATOR_ORGANIZATION = "asset:prop:originatorOrganization"; + public static final String VERSION = "asset:prop:version"; + public static final String CURATOR_ORGANIZATION_NAME = "asset:prop:curatorOrganizationName"; + public static final String LANGUAGE = "asset:prop:language"; + public static final String PUBLISHER = "asset:prop:publisher"; + public static final String STANDARD_LICENSE = "asset:prop:standardLicense"; + public static final String ENDPOINT_DOCUMENTATION = "asset:prop:endpointDocumentation"; + + public static final String DATA_CATEGORY = "http://w3id.org/mds#dataCategory"; + public static final String DATA_SUBCATEGORY = "http://w3id.org/mds#dataSubcategory"; + public static final String DATA_MODEL = "http://w3id.org/mds#dataModel"; + public static final String GEO_REFERENCE_METHOD = "http://w3id.org/mds#geoReferenceMethod"; + public static final String TRANSPORT_MODE = "http://w3id.org/mds#transportMode"; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java new file mode 100644 index 000000000..dafeef790 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao; + +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import org.jooq.DSLContext; + +import java.util.Collection; +import java.util.Set; + +public class ConnectorQueries { + + public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { + var c = Tables.CONNECTOR; + return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); + } + + public Set findConnectorsForScheduledRefresh(DSLContext dsl) { + var c = Tables.CONNECTOR; + return dsl.select(c.ENDPOINT).from(c).fetchSet(c.ENDPOINT); + } + + public Set findExistingConnectors(DSLContext dsl, Collection connectorEndpoints) { + var c = Tables.CONNECTOR; + return dsl.select(c.ENDPOINT).from(c) + .where(PostgresqlUtils.in(c.ENDPOINT, connectorEndpoints)) + .fetchSet(c.ENDPOINT); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferContractOfferQueries.java similarity index 94% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferContractOfferQueries.java index 2d415fa99..c02d0d52c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferContractOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferContractOfferQueries.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries; +package de.sovity.edc.ext.brokerserver.dao; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java new file mode 100644 index 000000000..76c47582a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.List; + +@RequiredArgsConstructor +public class DataOfferQueries { + + public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { + var d = Tables.DATA_OFFER; + return dsl.selectFrom(d).where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java new file mode 100644 index 000000000..3df3c2ec0 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import lombok.RequiredArgsConstructor; +import org.jooq.Field; +import org.jooq.JSON; +import org.jooq.impl.DSL; +import org.jooq.util.postgres.PostgresDSL; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogQueryAvailableFilterFetcher { + private final CatalogQueryFilterService catalogQueryFilterService; + + /** + * Query available filter values. + * + * @param fields query fields + * @param filter general filter to narrow results down to + * @param availableFilterValuesQueries one entry for each filter + * @return {@link Field} with field[iFilter][iValue] + */ + public Field queryAvailableFilterValues( + CatalogQueryFields fields, + CatalogQueryFilter filter, + List availableFilterValuesQueries + ) { + var c = fields.getConnectorTable(); + var d = fields.getDataOfferTable(); + + var valuesPerFilterAttribute = availableFilterValuesQueries.stream() + .map(it -> it.getAttributeValueField(fields)) + .map(PostgresDSL::arrayAggDistinct) + .toList(); + + return DSL.select(DSL.jsonArray(valuesPerFilterAttribute)) + .from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) + .where(catalogQueryFilterService.filter(fields, filter)) + .asField(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java new file mode 100644 index 000000000..334b345d3 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; +import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import lombok.RequiredArgsConstructor; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogQueryContractOfferFetcher { + + /** + * Query a data offer's contract offers. + * + * @param fields query fields + * @return {@link Field} of {@link ContractOfferRs}s + */ + public Field> getContractOffers(CatalogQueryFields fields) { + var c = fields.getConnectorTable(); + var d = fields.getDataOfferTable(); + var co = Tables.DATA_OFFER_CONTRACT_OFFER; + + var query = DSL.select( + co.CONTRACT_OFFER_ID, + co.POLICY.cast(String.class).as("policyJson"), + co.CREATED_AT, + co.UPDATED_AT + ).from(co).where( + co.CONNECTOR_ENDPOINT.eq(c.ENDPOINT), + co.ASSET_ID.eq(d.ASSET_ID)).orderBy(co.CREATED_AT.desc() + ); + + return MultisetUtils.multiset(query, ContractOfferRs.class); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java new file mode 100644 index 000000000..b0c07dc34 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; +import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import lombok.RequiredArgsConstructor; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogQueryDataOfferFetcher { + private final CatalogQuerySortingService catalogQuerySortingService; + private final CatalogQueryFilterService catalogQueryFilterService; + private final CatalogQueryContractOfferFetcher catalogQueryContractOfferFetcher; + + /** + * Query data offers + * + * @param fields query fields + * @param filter filter + * @param sorting sorting + * @return {@link Field} of {@link DataOfferRs}s + */ + public Field> queryDataOffers(CatalogQueryFields fields, CatalogQueryFilter filter, CatalogPageSortingType sorting) { + var c = fields.getConnectorTable(); + var d = fields.getDataOfferTable(); + + var query = DSL.select( + fields.getAssetId().as("assetId"), + d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), + d.CREATED_AT, + d.UPDATED_AT, + catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), + c.ENDPOINT.as("connectorEndpoint"), + c.ONLINE_STATUS.as("connectorOnlineStatus"), + fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt") + ) + .from(d) + .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) + .where(catalogQueryFilterService.filter(fields, filter)) + .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)); + + return MultisetUtils.multiset(query, DataOfferRs.class); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java new file mode 100644 index 000000000..4ee1e45c2 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import com.github.t9t.jooq.json.JsonbDSL; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.time.OffsetDateTime; + +/** + * Tables and fields used in the catalog page query. + *

+ * Having this as a class makes access to computed fields (e.g. asset properties) easier. + */ +@Getter +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class CatalogQueryFields { + Connector connectorTable; + DataOffer dataOfferTable; + + // Asset Properties from JSON to be used in sorting / filtering + Field assetId; + Field assetTitle; + Field assetDescription; + Field assetKeywords; + + // This date should always be non-null + // It's used in the UI to display the last relevant change date of a connector + Field offlineSinceOrLastUpdatedAt; + + public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable) { + this.connectorTable = connectorTable; + this.dataOfferTable = dataOfferTable; + assetId = dataOfferTable.ASSET_ID; + assetTitle = DSL.coalesce(getAssetProperty(AssetProperty.ASSET_NAME), assetId); + assetDescription = getAssetProperty(AssetProperty.DESCRIPTION); + assetKeywords = getAssetProperty(AssetProperty.KEYWORDS); + offlineSinceOrLastUpdatedAt = DSL.coalesce( + connectorTable.LAST_SUCCESSFUL_REFRESH_AT, + connectorTable.CREATED_AT + ); + } + + public Field getAssetProperty(String name) { + return JsonbDSL.fieldByKeyText(dataOfferTable.ASSET_PROPERTIES, name); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java new file mode 100644 index 000000000..a70c92ae8 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jooq.Condition; +import org.jooq.impl.DSL; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class CatalogQueryFilterService { + private final BrokerServerSettings brokerServerSettings; + + public Condition filter(CatalogQueryFields fields, CatalogQueryFilter filter) { + var conditions = new ArrayList(); + conditions.add(SearchUtils.simpleSearch(filter.searchQuery(), List.of( + fields.getAssetId(), + fields.getAssetTitle(), + fields.getAssetDescription(), + fields.getAssetKeywords(), + fields.getConnectorTable().ENDPOINT + ))); + conditions.add(onlyOnlineOrRecentlyOfflineConnectors(fields.getConnectorTable())); + conditions.addAll(filter.selectedFilters().stream().map(it -> it.filterDataOffers(fields)).toList()); + return DSL.and(conditions); + } + + @NotNull + private Condition onlyOnlineOrRecentlyOfflineConnectors(Connector c) { + var maxOfflineDuration = brokerServerSettings.getHideOfflineDataOffersAfter(); + + Condition maxOfflineDurationNotExceeded; + if (maxOfflineDuration == null) { + maxOfflineDurationNotExceeded = DSL.trueCondition(); + } else { + maxOfflineDurationNotExceeded = c.LAST_SUCCESSFUL_REFRESH_AT.greaterThan(OffsetDateTime.now().minus(maxOfflineDuration)); + } + + return DSL.or( + c.ONLINE_STATUS.eq(ConnectorOnlineStatus.ONLINE), + maxOfflineDurationNotExceeded + ); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java new file mode 100644 index 000000000..8a96763d7 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogPageRs; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogQueryService { + private final CatalogQueryDataOfferFetcher catalogQueryDataOfferFetcher; + private final CatalogQueryAvailableFilterFetcher catalogQueryAvailableFilterFetcher; + + /** + * Query all data required for the catalog page + * + * @param dsl transaction + * @param filter filter + * @param sorting sorting + * @param availableFilterValueQueries available filter value queries + * @return {@link CatalogPageRs} + */ + public CatalogPageRs queryCatalogPage( + DSLContext dsl, + CatalogQueryFilter filter, + CatalogPageSortingType sorting, + List availableFilterValueQueries + ) { + var fields = new CatalogQueryFields(Tables.CONNECTOR, Tables.DATA_OFFER); + + var availableFilterValues = catalogQueryAvailableFilterFetcher + .queryAvailableFilterValues(fields, filter, availableFilterValueQueries); + + var dataOffers = catalogQueryDataOfferFetcher.queryDataOffers(fields, filter, sorting); + + return dsl.select( + dataOffers.as("dataOffers"), + availableFilterValues.as("availableFilterValues") + ).fetchOneInto(CatalogPageRs.class); + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java new file mode 100644 index 000000000..3cc92a1d6 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jooq.OrderField; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogQuerySortingService { + @NotNull + public List> getOrderBy(CatalogQueryFields fields, CatalogPageSortingType sorting) { + List> orderBy; + if (sorting == null || sorting == CatalogPageSortingType.TITLE) { + orderBy = List.of( + fields.getAssetTitle().asc(), + fields.getConnectorTable().ENDPOINT.asc() + ); + } else if (sorting == CatalogPageSortingType.MOST_RECENT) { + orderBy = List.of( + fields.getDataOfferTable().CREATED_AT.desc(), + fields.getConnectorTable().ENDPOINT.asc() + ); + } else if (sorting == CatalogPageSortingType.ORIGINATOR) { + orderBy = List.of( + fields.getConnectorTable().ENDPOINT.asc(), + fields.getAssetTitle().asc() + ); + } else { + throw new IllegalArgumentException("Unknown %s: %s".formatted(CatalogPageSortingType.class.getName(), sorting)); + } + return orderBy; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java new file mode 100644 index 000000000..9dabec513 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import org.jooq.Field; + +@FunctionalInterface +public interface AvailableFilterValuesQuery { + + /** + * Gets the values for a given filter attribute from a list of data offers. + * + * @param fields a + * @return field / multiset field that will contain the available values + */ + Field getAttributeValueField(CatalogQueryFields fields); +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java new file mode 100644 index 000000000..d20d950a7 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CatalogPageRs { + String availableFilterValues; + List dataOffers; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java new file mode 100644 index 000000000..dc1b41a2a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; + +import java.util.List; + +public record CatalogQueryFilter( + String searchQuery, + List selectedFilters +) { + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java new file mode 100644 index 000000000..a3b2bcfa3 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import org.jooq.Condition; + +@FunctionalInterface +public interface CatalogQuerySelectedFilterQuery { + + /** + * Adds a filter to a Catalog Query. + * + * @param fields fields and tables available in the catalog query + * @return {@link Condition} + */ + Condition filterDataOffers(CatalogQueryFields fields); +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/ContractOfferRs.java similarity index 87% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/ContractOfferRs.java index 9572388b6..44b940286 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferContractOfferDbRow.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/ContractOfferRs.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.models; +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; import lombok.AccessLevel; import lombok.Getter; @@ -24,7 +24,7 @@ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) -public class DataOfferContractOfferDbRow { +public class ContractOfferRs { String contractOfferId; String policyJson; OffsetDateTime createdAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferRs.java similarity index 87% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferRs.java index 6f49ebe4c..c55fa0067 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/DataOfferDbRow.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferRs.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.models; +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import lombok.AccessLevel; @@ -26,12 +26,12 @@ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) -public class DataOfferDbRow { +public class DataOfferRs { String assetId; String assetPropertiesJson; OffsetDateTime createdAt; OffsetDateTime updatedAt; - List contractOffers; + List contractOffers; String connectorEndpoint; ConnectorOnlineStatus connectorOnlineStatus; OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java similarity index 56% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java index 448c93b3b..4d842049f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java @@ -12,14 +12,12 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries; +package de.sovity.edc.ext.brokerserver.dao.pages.connector; -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorPageDbRow; -import de.sovity.edc.ext.brokerserver.dao.queries.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.dao.queries.utils.SearchUtils; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; +import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -27,42 +25,17 @@ import org.jooq.OrderField; import org.jooq.impl.DSL; -import java.util.Collection; import java.util.List; -import java.util.Set; -import java.util.stream.Stream; -public class ConnectorQueries { - - public Stream findAll(DSLContext dsl) { - return dsl.selectFrom(Tables.CONNECTOR).stream(); - } - - public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { - var c = Tables.CONNECTOR; - return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); - } - - public Set findConnectorsForScheduledRefresh(DSLContext dsl) { - var c = Tables.CONNECTOR; - return dsl.select(c.ENDPOINT).from(c).fetchSet(c.ENDPOINT); - } - - public Set findExistingConnectors(DSLContext dsl, Collection connectorEndpoints) { - var c = Tables.CONNECTOR; - return dsl.select(c.ENDPOINT).from(c) - .where(PostgresqlUtils.in(c.ENDPOINT, connectorEndpoints)) - .fetchSet(c.ENDPOINT); - } - - public List forConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { +public class ConnectorPageQueryService { + public List queryConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of(c.ENDPOINT, c.CONNECTOR_ID)); return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) .from(c) .where(filterBySearchQuery) .orderBy(sortConnectorPage(c, sorting)) - .fetchInto(ConnectorPageDbRow.class); + .fetchInto(ConnectorRs.class); } @NotNull diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorRs.java similarity index 90% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorRs.java index 6a64fd22e..97664ae8e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/models/ConnectorPageDbRow.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorRs.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.models; +package de.sovity.edc.ext.brokerserver.dao.pages.connector.model; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import lombok.AccessLevel; @@ -25,7 +25,7 @@ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) -public class ConnectorPageDbRow { +public class ConnectorRs { String endpoint; String connectorId; private OffsetDateTime createdAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java deleted file mode 100644 index eb4bac1bc..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/DataOfferQueries.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.queries; - -import com.github.t9t.jooq.json.JsonbDSL; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; -import de.sovity.edc.ext.brokerserver.dao.models.DataOfferContractOfferDbRow; -import de.sovity.edc.ext.brokerserver.dao.models.DataOfferDbRow; -import de.sovity.edc.ext.brokerserver.dao.queries.utils.SearchUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jooq.Condition; -import org.jooq.DSLContext; -import org.jooq.Field; -import org.jooq.OrderField; -import org.jooq.impl.DSL; - -import java.time.OffsetDateTime; -import java.util.List; - -@RequiredArgsConstructor -public class DataOfferQueries { - private final BrokerServerSettings brokerServerSettings; - - public List forCatalogPage(DSLContext dsl, String searchQuery, CatalogPageSortingType sorting) { - var c = Tables.CONNECTOR; - var d = Tables.DATA_OFFER; - - // Asset Properties from JSON to be used in sorting / filtering - var assetId = JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.ASSET_ID); - var assetTitle = DSL.coalesce(JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.ASSET_NAME), assetId); - var assetDescription = JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.DESCRIPTION); - var assetKeywords = JsonbDSL.fieldByKeyText(d.ASSET_PROPERTIES, AssetProperty.KEYWORDS); - - // This date should always be non-null - // It's used in the UI to display the last relevant change date of a connector - var offlineSinceOrLastUpdatedAt = DSL.coalesce( - c.LAST_SUCCESSFUL_REFRESH_AT, - c.CREATED_AT - ); - - var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( - assetId, - assetTitle, - assetDescription, - assetKeywords, - c.ENDPOINT - )); - - return dsl.select( - assetId.as("assetId"), - d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), - d.CREATED_AT, - d.UPDATED_AT, - getContractOffers(d.CONNECTOR_ENDPOINT, d.ASSET_ID).as("contractOffers"), - c.ENDPOINT.as("connectorEndpoint"), - c.ONLINE_STATUS.as("connectorOnlineStatus"), - offlineSinceOrLastUpdatedAt.as("connectorOfflineSinceOrLastUpdatedAt") - ) - .from(d) - .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) - .where( - filterBySearchQuery, - onlyOnlineOrRecentlyOfflineConnectors(c) - ) - .orderBy(getOrderBy(sorting, c, d, assetTitle)) - .fetchInto(DataOfferDbRow.class); - } - - @NotNull - private Condition onlyOnlineOrRecentlyOfflineConnectors(Connector c) { - var maxOfflineDuration = brokerServerSettings.getHideOfflineDataOffersAfter(); - - Condition maxOfflineDurationNotExceeded; - if (maxOfflineDuration == null) { - maxOfflineDurationNotExceeded = DSL.trueCondition(); - } else { - maxOfflineDurationNotExceeded = c.LAST_SUCCESSFUL_REFRESH_AT.greaterThan(OffsetDateTime.now().minus(maxOfflineDuration)); - } - - return DSL.or( - c.ONLINE_STATUS.eq(ConnectorOnlineStatus.ONLINE), - maxOfflineDurationNotExceeded - ); - } - - @NotNull - private List> getOrderBy(CatalogPageSortingType sorting, Connector c, DataOffer d, Field assetTitle) { - List> orderBy; - if (sorting == null || sorting == CatalogPageSortingType.TITLE) { - orderBy = List.of(assetTitle.asc(), c.ENDPOINT.asc()); - } else if (sorting == CatalogPageSortingType.MOST_RECENT) { - orderBy = List.of(d.CREATED_AT.desc(), c.ENDPOINT.asc()); - } else if (sorting == CatalogPageSortingType.ORIGINATOR) { - orderBy = List.of(c.ENDPOINT.asc(), assetTitle.asc()); - } else { - throw new IllegalArgumentException("Unknown %s: %s".formatted(CatalogPageSortingType.class.getName(), sorting)); - } - return orderBy; - } - - private Field> getContractOffers(Field connectorEndpoint, Field assetId) { - var dco = Tables.DATA_OFFER_CONTRACT_OFFER; - - var query = DSL.select( - dco.CONTRACT_OFFER_ID, - dco.POLICY.cast(String.class).as("policyJson"), - dco.CREATED_AT, - dco.UPDATED_AT - ).from(dco).where( - dco.CONNECTOR_ENDPOINT.eq(connectorEndpoint), - dco.ASSET_ID.eq(assetId)).orderBy(dco.CREATED_AT.desc() - ); - - return DSL.multiset(query).convertFrom(it -> it.into(DataOfferContractOfferDbRow.class)); - } - - public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { - var d = Tables.DATA_OFFER; - return dsl.selectFrom(d).where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java new file mode 100644 index 000000000..3803c91fe --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.utils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + +import java.util.List; + +/** + * Some things are easier to fetch as json into a string with JooQ. + * In that case we need to deserialize that string into an object of our choice afterwards. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonDeserializationUtils { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference>> TYPE_STRING_LIST_2 = new TypeReference<>() { + }; + + @SneakyThrows + public static List> deserializeStringArray2(String json) { + return OBJECT_MAPPER.readValue(json, TYPE_STRING_LIST_2); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonbUtils.java similarity index 93% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonbUtils.java index 42dff4864..0a5157624 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/JsonbUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonbUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries.utils; +package de.sovity.edc.ext.brokerserver.dao.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java similarity index 96% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java index 5a76127f5..04fabd3db 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries.utils; +package de.sovity.edc.ext.brokerserver.dao.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java new file mode 100644 index 000000000..9cdcdd84d --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.jooq.Field; +import org.jooq.TableLike; +import org.jooq.impl.DSL; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MultisetUtils { + public static Field> multiset(TableLike table, Class type) { + return DSL.multiset(table).convertFrom(it -> it.into(type)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/PostgresqlUtils.java similarity index 94% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/PostgresqlUtils.java index dd321d475..e5291de5e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/PostgresqlUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/PostgresqlUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries.utils; +package de.sovity.edc.ext.brokerserver.dao.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java similarity index 96% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java index 5b917d9ff..e8a6ef310 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/SearchUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries.utils; +package de.sovity.edc.ext.brokerserver.dao.utils; import de.sovity.edc.ext.brokerserver.utils.StringUtils2; import lombok.AccessLevel; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index 2bb4323d0..8d87f2c51 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services; -import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index bb9e70d0b..cc65abf06 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -14,14 +14,15 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.models.DataOfferContractOfferDbRow; -import de.sovity.edc.ext.brokerserver.dao.models.DataOfferDbRow; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; +import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingItem; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; -import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilter; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferListEntry; import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferListEntryContractOffer; @@ -35,49 +36,61 @@ @RequiredArgsConstructor public class CatalogApiService { private final PaginationMetadataUtils paginationMetadataUtils; - private final DataOfferQueries dataOfferQueries; + private final CatalogQueryService catalogQueryService; private final PolicyDtoBuilder policyDtoBuilder; private final AssetPropertyParser assetPropertyParser; + private final CatalogFilterService catalogFilterService; public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { Objects.requireNonNull(query, "query must not be null"); - var dataOfferDbRows = dataOfferQueries.forCatalogPage(dsl, query.getSearchQuery(), query.getSorting()); + + var filter = new CatalogQueryFilter( + query.getSearchQuery(), + catalogFilterService.getSelectedFiltersQuery(query.getFilter()) + ); + + var catalogPageRs = catalogQueryService.queryCatalogPage( + dsl, + filter, + query.getSorting(), + catalogFilterService.getAvailableFiltersQuery() + ); var result = new CatalogPageResult(); result.setAvailableSortings(buildAvailableSortings()); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(dataOfferDbRows.size())); - result.setAvailableFilters(new CnfFilter(List.of())); - result.setDataOffers(buildDataOfferListEntries(dataOfferDbRows)); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(catalogPageRs.getDataOffers().size())); + result.setAvailableFilters(catalogFilterService.buildAvailableFilters(catalogPageRs.getAvailableFilterValues())); + result.setDataOffers(buildDataOfferListEntries(catalogPageRs.getDataOffers())); return result; } - private List buildDataOfferListEntries(List dataOfferDbRows) { - return dataOfferDbRows.stream() + private List buildDataOfferListEntries(List dataOfferRs) { + return dataOfferRs.stream() .map(this::buildDataOfferListEntry) .toList(); } - private DataOfferListEntry buildDataOfferListEntry(DataOfferDbRow dataOfferDbRow) { + private DataOfferListEntry buildDataOfferListEntry(DataOfferRs dataOfferRs) { var dataOffer = new DataOfferListEntry(); - dataOffer.setAssetId(dataOfferDbRow.getAssetId()); - dataOffer.setCreatedAt(dataOfferDbRow.getCreatedAt()); - dataOffer.setUpdatedAt(dataOfferDbRow.getUpdatedAt()); - dataOffer.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferDbRow.getAssetPropertiesJson())); - dataOffer.setContractOffers(buildDataOfferListEntryContractOffers(dataOfferDbRow)); - dataOffer.setConnectorEndpoint(dataOfferDbRow.getConnectorEndpoint()); - dataOffer.setConnectorOfflineSinceOrLastUpdatedAt(dataOfferDbRow.getConnectorOfflineSinceOrLastUpdatedAt()); - dataOffer.setConnectorOnlineStatus(getOnlineStatus(dataOfferDbRow)); + dataOffer.setAssetId(dataOfferRs.getAssetId()); + dataOffer.setCreatedAt(dataOfferRs.getCreatedAt()); + dataOffer.setUpdatedAt(dataOfferRs.getUpdatedAt()); + dataOffer.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferRs.getAssetPropertiesJson())); + dataOffer.setContractOffers(buildDataOfferListEntryContractOffers(dataOfferRs)); + dataOffer.setConnectorEndpoint(dataOfferRs.getConnectorEndpoint()); + dataOffer.setConnectorOfflineSinceOrLastUpdatedAt(dataOfferRs.getConnectorOfflineSinceOrLastUpdatedAt()); + dataOffer.setConnectorOnlineStatus(getOnlineStatus(dataOfferRs)); return dataOffer; } - private List buildDataOfferListEntryContractOffers(DataOfferDbRow dataOfferDbRow) { - return dataOfferDbRow.getContractOffers().stream() + private List buildDataOfferListEntryContractOffers(DataOfferRs dataOfferRs) { + return dataOfferRs.getContractOffers().stream() .map(this::buildDataOfferListEntryContractOffer) .toList(); } - private DataOfferListEntryContractOffer buildDataOfferListEntryContractOffer(DataOfferContractOfferDbRow contractOfferDbRow) { + private DataOfferListEntryContractOffer buildDataOfferListEntryContractOffer(ContractOfferRs contractOfferDbRow) { var contractOffer = new DataOfferListEntryContractOffer(); contractOffer.setContractOfferId(contractOfferDbRow.getContractOfferId()); contractOffer.setContractPolicy(policyDtoBuilder.buildPolicyFromJson(contractOfferDbRow.getPolicyJson())); @@ -86,11 +99,11 @@ private DataOfferListEntryContractOffer buildDataOfferListEntryContractOffer(Dat return contractOffer; } - private ConnectorOnlineStatus getOnlineStatus(DataOfferDbRow dataOfferDbRow) { - return switch (dataOfferDbRow.getConnectorOnlineStatus()) { + private ConnectorOnlineStatus getOnlineStatus(DataOfferRs dataOfferRs) { + return switch (dataOfferRs.getConnectorOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + dataOfferDbRow.getConnectorOnlineStatus()); + default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + dataOfferRs.getConnectorOnlineStatus()); }; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 19a4b7f2c..847eed0dd 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -14,8 +14,8 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.models.ConnectorPageDbRow; -import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; @@ -31,13 +31,13 @@ @RequiredArgsConstructor public class ConnectorApiService { - private final ConnectorQueries connectorQueries; + private final ConnectorPageQueryService connectorPageQueryService; private final PaginationMetadataUtils paginationMetadataUtils; public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery query) { Objects.requireNonNull(query, "query must not be null"); - var connectorDbRows = connectorQueries.forConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); + var connectorDbRows = connectorPageQueryService.queryConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); var result = new ConnectorPageResult(); result.setAvailableSortings(buildAvailableSortings()); @@ -46,27 +46,27 @@ public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery quer return result; } - private List buildConnectorListEntries(List connectorDbRows) { - return connectorDbRows.stream().map(this::buildConnectorListEntry).toList(); + private List buildConnectorListEntries(List connectors) { + return connectors.stream().map(this::buildConnectorListEntry).toList(); } - private ConnectorListEntry buildConnectorListEntry(ConnectorPageDbRow it) { + private ConnectorListEntry buildConnectorListEntry(ConnectorRs connector) { var dto = new ConnectorListEntry(); - dto.setId(it.getConnectorId()); - dto.setEndpoint(it.getEndpoint()); - dto.setCreatedAt(it.getCreatedAt()); - dto.setLastRefreshAttemptAt(it.getLastRefreshAttemptAt()); - dto.setLastSuccessfulRefreshAt(it.getLastSuccessfulRefreshAt()); - dto.setOnlineStatus(getOnlineStatus(it)); - dto.setNumContractOffers(it.getNumDataOffers()); + dto.setId(connector.getConnectorId()); + dto.setEndpoint(connector.getEndpoint()); + dto.setCreatedAt(connector.getCreatedAt()); + dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); + dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); + dto.setOnlineStatus(getOnlineStatus(connector)); + dto.setNumContractOffers(connector.getNumDataOffers()); return dto; } - private ConnectorOnlineStatus getOnlineStatus(ConnectorPageDbRow it) { - return switch (it.getOnlineStatus()) { + private ConnectorOnlineStatus getOnlineStatus(ConnectorRs connector) { + return switch (connector.getOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + it.getOnlineStatus()); + default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + connector.getOnlineStatus()); }; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java new file mode 100644 index 000000000..18c9218ba --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api.filtering; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import org.jooq.Condition; + +import java.util.Collection; + +@FunctionalInterface +public interface AttributeFilterQuery { + + /** + * Filters a Catalog DB Query for a given Filter Attribute with selected values + * + * @param fields available tables and fields during the catalog query + * @param values values to be filtered by. Usually this should mean that only one of the values needs to be present. + * @return {@link Condition} + */ + Condition filterDataOffers(CatalogQueryFields fields, Collection values); + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java new file mode 100644 index 000000000..9d13a2c00 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api.filtering; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; + +/** + * Implementation of a filter attribute definition for the catalog. + * + * @param name technical id of the attribute + * @param label UI showing label for the attribute + * @param valueGetter query existing values from DB + * @param filterApplier apply a filter to a data offer query + */ +public record CatalogFilterAttributeDefinition( + String name, + String label, + AvailableFilterValuesQuery valueGetter, + AttributeFilterQuery filterApplier +) { +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java new file mode 100644 index 000000000..3b4cbed31 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api.filtering; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import org.jetbrains.annotations.NotNull; +import org.jooq.Field; +import org.jooq.impl.DSL; + +public class CatalogFilterAttributeDefinitionService { + + public CatalogFilterAttributeDefinition fromAssetProperty(String assetProperty, String label) { + return new CatalogFilterAttributeDefinition( + assetProperty, + label, + fields -> getValue(fields, assetProperty), + (fields, values) -> PostgresqlUtils.in(getValue(fields, assetProperty), values) + ); + } + + @NotNull + private Field getValue(CatalogQueryFields fields, String assetProperty) { + return DSL.coalesce(fields.getAssetProperty(assetProperty), DSL.value("")); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java new file mode 100644 index 000000000..266724354 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api.filtering; + +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; +import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; +import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; +import de.sovity.edc.ext.brokerserver.utils.MapUtils; +import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilter; +import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterAttribute; +import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterItem; +import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterValue; +import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterValueAttribute; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; +import org.jooq.impl.DSL; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class CatalogFilterService { + private final CatalogFilterAttributeDefinitionService catalogFilterAttributeDefinitionService; + + private final Comparator caseInsensitiveEmptyStringLast = (s1, s2) -> { + int result = s1.compareToIgnoreCase(s2); + if (s1.isEmpty() && !s2.isEmpty()) { + return 1; + } else if (!s1.isEmpty() && s2.isEmpty()) { + return -1; + } else { + return result; + } + }; + + + /** + * Currently supported filters for the catalog page. + * + * @return attribute definitions + */ + private List getAvailableFilters() { + return List.of( + catalogFilterAttributeDefinitionService.fromAssetProperty( + AssetProperty.DATA_CATEGORY, + "Data Category" + ), + catalogFilterAttributeDefinitionService.fromAssetProperty( + AssetProperty.DATA_SUBCATEGORY, + "Data Subcategory" + ), + catalogFilterAttributeDefinitionService.fromAssetProperty( + AssetProperty.DATA_MODEL, + "Data Model" + ), + catalogFilterAttributeDefinitionService.fromAssetProperty( + AssetProperty.TRANSPORT_MODE, + "Transport Mode" + ), + catalogFilterAttributeDefinitionService.fromAssetProperty( + AssetProperty.GEO_REFERENCE_METHOD, + "Geo Reference Method" + ) + ); + } + + public List getAvailableFiltersQuery() { + return getAvailableFilters().stream().map(CatalogFilterAttributeDefinition::valueGetter).toList(); + } + + public CnfFilter buildAvailableFilters(String filterValuesJson) { + var filterValues = JsonDeserializationUtils.deserializeStringArray2(filterValuesJson); + var filterAttributes = zipAvailableFilters(getAvailableFilters(), filterValues) + .map(availableFilter -> new CnfFilterAttribute( + availableFilter.definition().name(), + availableFilter.definition().label(), + buildAvailableFilterValues(availableFilter) + )) + .toList(); + return new CnfFilter(filterAttributes); + } + + + public List getSelectedFiltersQuery(CnfFilterValue selectedFilters) { + if (selectedFilters == null || selectedFilters.getSelectedAttributeValues() == null) { + return List.of(); + } + + var availableFilters = MapUtils.associateBy(getAvailableFilters(), CatalogFilterAttributeDefinition::name); + return selectedFilters.getSelectedAttributeValues().stream() + .filter(selectedFilter -> CollectionUtils2.isNotEmpty(selectedFilter.getSelectedIds())) + .map(selectedFilter -> buildSelectedFilter(availableFilters, selectedFilter)) + .toList(); + } + + private List buildAvailableFilterValues(AvailableFilter availableFilter) { + return availableFilter.availableValues().stream() + .sorted(caseInsensitiveEmptyStringLast) + .map(value -> new CnfFilterItem(value, value)) + .toList(); + } + + private Stream zipAvailableFilters(List availableFilters, List> filterValues) { + Validate.isTrue( + availableFilters.size() == filterValues.size(), + "Number of available filters and filter values must match: %d != %d", + availableFilters.size(), + filterValues.size() + ); + return Stream.iterate(0, i -> i + 1) + .limit(availableFilters.size()) + .map(i -> new AvailableFilter(availableFilters.get(i), filterValues.get(i))); + } + + private record AvailableFilter(CatalogFilterAttributeDefinition definition, List availableValues) { + } + + @NotNull + private CatalogQuerySelectedFilterQuery buildSelectedFilter(Map availableFilters, CnfFilterValueAttribute selected) { + var available = availableFilters.get(selected.getId()); + if (available == null) { + return fields -> DSL.falseCondition(); + } + return fields -> available.filterApplier().filterDataOffers(fields, selected.getSelectedIds()); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java index 909c8ced1..d03f58ffa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.queue; -import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index 203756ad3..591c45f34 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing; -import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java index fa6a4cebc..8e9854f79 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.queries.utils.JsonbUtils; +import de.sovity.edc.ext.brokerserver.dao.utils.JsonbUtils; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java index 96da9969e..959885022 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java @@ -14,8 +14,8 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferContractOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.DataOfferPatch; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java index 73a33c214..283e8563c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.queries.utils.JsonbUtils; +import de.sovity.edc.ext.brokerserver.dao.utils.JsonbUtils; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; import lombok.RequiredArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java index cdf3c2150..e9b8a0b18 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java @@ -37,4 +37,8 @@ public static Set difference(@NonNull Collection a, @NonNull Collectio result.removeAll(b); return result; } + + public static boolean isNotEmpty(Collection collection) { + return collection != null && !collection.isEmpty(); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java similarity index 91% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java rename to extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java index ebb127206..13fe324ec 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/queries/utils/LikeUtilsTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.queries.utils; +package de.sovity.edc.ext.brokerserver.dao.utils; import org.junit.jupiter.api.Test; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java index 416fde00e..059f81474 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java @@ -16,7 +16,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.eclipse.edc.monitor.logger.LoggerMonitor; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; import org.eclipse.edc.spi.system.configuration.Config; import static org.mockito.ArgumentMatchers.any; @@ -28,7 +28,7 @@ public class FlywayTestUtils { public static void migrate(TestDatabase testDatabase) { - var monitor = new LoggerMonitor(); + var monitor = new ConsoleMonitor(); var config = mock(Config.class); when(config.getBoolean(eq(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE), any())).thenReturn(true); when(config.getBoolean(eq(PostgresFlywayExtension.FLYWAY_CLEAN), any())).thenReturn(true); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index f91b663a5..d837f82b2 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -16,15 +16,23 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.client.gen.model.CatalogPageQuery; +import de.sovity.edc.client.gen.model.CatalogPageResult; +import de.sovity.edc.client.gen.model.CnfFilterAttribute; +import de.sovity.edc.client.gen.model.CnfFilterItem; +import de.sovity.edc.client.gen.model.CnfFilterValue; +import de.sovity.edc.client.gen.model.CnfFilterValueAttribute; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.SneakyThrows; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.policy.model.Policy; +import org.jooq.DSLContext; import org.jooq.JSONB; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,6 +40,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.time.OffsetDateTime; +import java.util.List; import java.util.Map; import static de.sovity.edc.client.gen.model.DataOfferListEntry.ConnectorOnlineStatusEnum.ONLINE; @@ -52,38 +61,16 @@ void setUp(EdcExtension extension) { } @Test - void testQueryConnectors() { + void testDataOfferDetails() { TEST_DATABASE.testTransaction(dsl -> { // arrange var today = OffsetDateTime.now().withNano(0); - var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setConnectorId("http://my-connector"); - connector.setEndpoint("http://my-connector/ids/data"); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(today.minusDays(1)); - connector.setLastRefreshAttemptAt(today); - connector.setLastSuccessfulRefreshAt(today); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - connector.insert(); - - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - dataOffer.setAssetId("urn:artifact:my-asset"); - dataOffer.setAssetProperties(JSONB.jsonb("{\"asset:prop:id\": \"urn:artifact:my-asset\", \"asset:prop:name\": \"my-asset\"}")); - dataOffer.setConnectorEndpoint("http://my-connector/ids/data"); - dataOffer.setCreatedAt(today.minusDays(5)); - dataOffer.setUpdatedAt(today); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); - contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint("http://my-connector/ids/data"); - contractOffer.setAssetId("urn:artifact:my-asset"); - contractOffer.setCreatedAt(today.minusDays(5)); - contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); - contractOffer.insert(); + createConnector(dsl, today); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" + )); var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -95,14 +82,154 @@ void testQueryConnectors() { assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(ONLINE); assertThat(dataOfferResult.getAssetId()).isEqualTo("urn:artifact:my-asset"); assertThat(dataOfferResult.getProperties()).isEqualTo(Map.of( - "asset:prop:id", "urn:artifact:my-asset", - "asset:prop:name", "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" )); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); assertThat(toJson(dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy())).isEqualTo(toJson(dummyPolicy())); }); } + @Test + void testAvailableFilters_noFilter() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + )); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", + AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + )); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" + )); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "" + )); + + + var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + + assertThat(result.getAvailableFilters().getFields()) + .extracting(CnfFilterAttribute::getId) + .containsExactly( + AssetProperty.DATA_CATEGORY, + AssetProperty.DATA_SUBCATEGORY, + AssetProperty.DATA_MODEL, + AssetProperty.TRANSPORT_MODE, + AssetProperty.GEO_REFERENCE_METHOD + ); + + assertThat(result.getAvailableFilters().getFields()) + .extracting(CnfFilterAttribute::getTitle) + .containsExactly( + "Data Category", + "Data Subcategory", + "Data Model", + "Transport Mode", + "Geo Reference Method" + ); + + var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); + assertThat(dataCategory.getTitle()).isEqualTo("Data Category"); + assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-category-1"); + assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-category-1"); + + var transportMode = getAvailableFilter(result, AssetProperty.TRANSPORT_MODE); + assertThat(transportMode.getTitle()).isEqualTo("Transport Mode"); + assertThat(transportMode.getValues()).extracting(CnfFilterItem::getId).containsExactly("MY-TRANSPORT-MODE-1", "my-transport-mode-2", ""); + assertThat(transportMode.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MY-TRANSPORT-MODE-1", "my-transport-mode-2", ""); + + var dataSubcategory = getAvailableFilter(result, AssetProperty.DATA_SUBCATEGORY); + assertThat(dataSubcategory.getTitle()).isEqualTo("Data Subcategory"); + assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); + assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); + }); + } + + private CnfFilterAttribute getAvailableFilter(CatalogPageResult result, String filterId) { + return result.getAvailableFilters().getFields().stream() + .filter(it -> it.getId().equals(filterId)).findFirst() + .orElseThrow(() -> new IllegalStateException("Filter not found")); + } + + @Test + void testAvailableFilters_withFilter() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category" + )); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2" + )); + + + var query = new CatalogPageQuery(); + query.setFilter(new CnfFilterValue(List.of( + new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) + ))); + + var result = edcClient().brokerServerApi().catalogPage(query); + var actual = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); + + assertThat(actual.getId()).isEqualTo(AssetProperty.DATA_CATEGORY); + assertThat(actual.getTitle()).isEqualTo("Data Category"); + assertThat(actual.getValues()).extracting(CnfFilterItem::getId).containsExactly(""); + assertThat(actual.getValues()).extracting(CnfFilterItem::getTitle).containsExactly(""); + }); + } + + private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties) { + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + dataOffer.setConnectorEndpoint("http://my-connector/ids/data"); + dataOffer.setCreatedAt(today.minusDays(5)); + dataOffer.setUpdatedAt(today); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + contractOffer.setContractOfferId("my-contract-offer-1"); + contractOffer.setConnectorEndpoint("http://my-connector/ids/data"); + contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setCreatedAt(today.minusDays(5)); + contractOffer.setUpdatedAt(today); + contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.insert(); + } + + private void createConnector(DSLContext dsl, OffsetDateTime today) { + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setConnectorId("http://my-connector"); + connector.setEndpoint("http://my-connector/ids/data"); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setCreatedAt(today.minusDays(1)); + connector.setLastRefreshAttemptAt(today); + connector.setLastSuccessfulRefreshAt(today); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + connector.insert(); + } + private Policy dummyPolicy() { return Policy.Builder.newInstance() .assignee("Example Assignee") @@ -116,4 +243,9 @@ private String policyToJson(Policy policy) { private String toJson(Object o) { return new ObjectMapper().valueToTree(o).toString(); } + + @SneakyThrows + private String assetProperties(Map assetProperties) { + return new ObjectMapper().writeValueAsString(assetProperties); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java index fee32fbeb..5335f6899 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.brokerserver.services.refreshing.offers; import de.sovity.edc.ext.brokerserver.BrokerServerExtension; @@ -23,7 +37,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class DataOfferLimitsEnforcerTest { +class DataOfferLimitsEnforcerTest { DataOfferLimitsEnforcer dataOfferLimitsEnforcer; Config config; BrokerEventLogger brokerEventLogger; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java index 65be85c1d..601e66db0 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java @@ -15,7 +15,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; -import de.sovity.edc.ext.brokerserver.dao.queries.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java index 9bab06b9a..07f3cf5f7 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java @@ -14,8 +14,8 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferContractOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.queries.DataOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; import lombok.Value; import org.eclipse.edc.spi.system.configuration.Config; @@ -26,7 +26,7 @@ class DataOfferWriterTestDydi { Config config = mock(Config.class); BrokerServerSettings brokerServerSettings = new BrokerServerSettings(config); - DataOfferQueries dataOfferQueries = new DataOfferQueries(brokerServerSettings); + DataOfferQueries dataOfferQueries = new DataOfferQueries(); DataOfferContractOfferQueries dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); ContractOfferRecordUpdater contractOfferRecordUpdater = new ContractOfferRecordUpdater(); DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater(); From cbe0ea7ed4cd3c1a7ac81b142991709459b55530 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 14 Jun 2023 07:42:14 +0200 Subject: [PATCH 058/295] feat: data offer pagination (and sorting fixes) (#127) * feat: data offer pagination (and sorting fixes) * chore: add stubs for new endpoints * chore: test migrations so that non-empty tables will be migrated. --- connector/.env | 3 + .../build.gradle.kts | 3 +- ...erver_Initial_DB_Model.sql => V2__PoC.sql} | 31 +++----- .../main/resources/db/migration/V3_1__MvP.sql | 36 ++++++++++ .../migration/V3_2__MvP_Non_Transactional.sql | 10 +++ .../db/testdata/V2_1__PoC_Test_Data.sql | 69 ++++++++++++++++++ .../brokerserver/BrokerServerExtension.java | 3 + .../BrokerServerExtensionContextBuilder.java | 3 +- .../BrokerServerResourceImpl.java | 14 ++++ .../catalog/CatalogQueryDataOfferFetcher.java | 64 ++++++++++++----- .../dao/pages/catalog/CatalogQueryFields.java | 4 +- .../catalog/CatalogQueryFilterService.java | 2 +- .../pages/catalog/CatalogQueryService.java | 10 ++- .../catalog/CatalogQuerySortingService.java | 4 +- .../pages/catalog/models/CatalogPageRs.java | 1 + .../dao/pages/catalog/models/PageQuery.java | 18 +++++ .../services/BrokerServerSettings.java | 6 +- .../services/api/CatalogApiService.java | 18 ++++- .../services/api/PaginationMetadataUtils.java | 21 ++++++ .../refreshing/offers/DataOfferBuilder.java | 9 +++ .../offers/DataOfferRecordUpdater.java | 7 ++ .../offers/model/FetchedDataOffer.java | 1 + .../services/api/CatalogApiTest.java | 72 ++++++++++++++++++- .../offers/DataOfferWriterTestDataHelper.java | 35 +++++---- 24 files changed, 377 insertions(+), 67 deletions(-) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V2__Broker_Server_Initial_DB_Model.sql => V2__PoC.sql} (71%) create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java diff --git a/connector/.env b/connector/.env index 365149db4..a853dcd2b 100644 --- a/connector/.env +++ b/connector/.env @@ -30,6 +30,9 @@ EDC_BROKER_SERVER_NUM_THREADS=3 EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR=50 EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_CONNECTOR=10 +# Pagination Defaults +EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 + # =========================================================== # Other EDC Config diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index f8ae7639b..566d66222 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -8,6 +8,7 @@ val jdbcDriver = "org.postgresql.Driver" val postgresContainer = "postgres:11-alpine" val migrationsDir = "src/main/resources/db/migration" +val testDataDir = "src/main/resources/db/testdata" val jooqTargetPackage = "de.sovity.edc.ext.brokerserver.db.jooq" val jooqTargetSourceRoot = "build/generated/jooq" @@ -115,7 +116,7 @@ flyway { cleanDisabled = false cleanOnValidationError = true baselineOnMigrate = true - locations = arrayOf("filesystem:${migrationsDir}") + locations = arrayOf("filesystem:${migrationsDir}", "filesystem:${testDataDir}") configurations = arrayOf("flywayMigration") } diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql similarity index 71% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql index fcf8ee3e5..45481e629 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__Broker_Server_Initial_DB_Model.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql @@ -1,19 +1,15 @@ create type connector_online_status as enum ('ONLINE', 'OFFLINE'); create type measurement_type as enum ('CONNECTOR_REFRESH'); create type measurement_error_status as enum ('ERROR', 'OK'); -create type connector_data_offers_exceeded as enum ('OK', 'EXCEEDED'); -create type connector_contract_offers_exceeded as enum ('OK', 'EXCEEDED'); create table connector ( - endpoint text not null, - connector_id text not null, - created_at timestamp with time zone not null, + endpoint text not null, + connector_id text not null, + created_at timestamp with time zone not null, last_refresh_attempt_at timestamp with time zone, last_successful_refresh_at timestamp with time zone, - online_status connector_online_status not null, - data_offers_exceeded connector_data_offers_exceeded not null, - contract_offers_exceeded connector_contract_offers_exceeded not null, + online_status connector_online_status not null, PRIMARY KEY (endpoint) ); @@ -39,7 +35,7 @@ create table data_offer_contract_offer created_at timestamp with time zone not null, updated_at timestamp with time zone, - PRIMARY KEY (connector_endpoint, asset_id, contract_offer_id), + PRIMARY KEY (contract_offer_id), FOREIGN KEY (connector_endpoint, asset_id) REFERENCES data_offer (connector_endpoint, asset_id), FOREIGN KEY (connector_endpoint) REFERENCES connector (endpoint) ); @@ -61,19 +57,7 @@ create type broker_event_type as enum ( 'CONTRACT_OFFER_UPDATED', --Contract Offer was clicked - 'CONTRACT_OFFER_CLICK', - - --Connector Data Offer Limit was exceeded - 'CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED', - - --Connector Data Offer Limit was not exceeded - 'CONNECTOR_DATA_OFFER_LIMIT_OK', - - --Connector Contract Offer Limit was exceeded - 'CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED', - - --Connector Contract Offer Limit was not exceeded - 'CONNECTOR_CONTRACT_OFFER_LIMIT_OK' + 'CONTRACT_OFFER_CLICK' ); create type broker_event_status as enum ( @@ -93,7 +77,8 @@ create table broker_event_log event_status broker_event_status not null, connector_endpoint text, asset_id text, - error_stack text + error_stack text, + duration_in_ms bigint ); create table broker_execution_time_measurement diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql new file mode 100644 index 000000000..ecffc4e51 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql @@ -0,0 +1,36 @@ +create collation if not exists alphanumeric_with_natural_sort (provider = icu, locale = 'en-u-kn-true'); + +create type connector_data_offers_exceeded as enum ('OK', 'EXCEEDED'); +create type connector_contract_offers_exceeded as enum ('OK', 'EXCEEDED'); + +alter table broker_event_log + drop column duration_in_ms; + +alter table connector + alter column endpoint type text collate alphanumeric_with_natural_sort, + add column data_offers_exceeded connector_data_offers_exceeded, + add column contract_offers_exceeded connector_contract_offers_exceeded; + +update connector +set data_offers_exceeded = 'OK', + contract_offers_exceeded = 'OK'; + +alter table connector + alter column data_offers_exceeded set not null, + alter column contract_offers_exceeded set not null; + +alter table data_offer + alter column asset_id type text collate alphanumeric_with_natural_sort, + add column asset_name text collate alphanumeric_with_natural_sort; + +update data_offer +set asset_name = coalesce(asset_properties ->> 'asset:prop:name', asset_id); + +alter table data_offer + alter column asset_name set not null; + +-- update contract offer table's primary key +alter table data_offer_contract_offer + drop constraint data_offer_contract_offer_pkey; +alter table data_offer_contract_offer + add primary key (connector_endpoint, asset_id, contract_offer_id); diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql new file mode 100644 index 000000000..c39525a59 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql @@ -0,0 +1,10 @@ +-- Changes to Enums are non-transactional and must be supplied in a separate migration script for flyway + +-- Connector Data Offer Limit was exceeded +alter type broker_event_type add value 'CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED'; +-- Connector Data Offer Limit was not exceeded +alter type broker_event_type add value 'CONNECTOR_DATA_OFFER_LIMIT_OK'; +-- Connector Contract Offer Limit was exceeded +alter type broker_event_type add value 'CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED'; +-- Connector Contract Offer Limit was not exceeded +alter type broker_event_type add value 'CONNECTOR_CONTRACT_OFFER_LIMIT_OK'; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql new file mode 100644 index 000000000..b3235f637 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql @@ -0,0 +1,69 @@ +-- Test Data to be added after V2 so we can test subsequent migrations + +insert into connector (endpoint, connector_id, created_at, last_refresh_attempt_at, last_successful_refresh_at, + online_status) +values ('https://my-connector.com/ids/data', 'test-connector-1', '2019-01-01 00:00:00', + '2019-01-01 00:00:00', '2019-01-01 00:00:00', 'ONLINE'); +insert into data_offer (connector_endpoint, asset_id, asset_properties, created_at, updated_at) +values ('https://my-connector.com/ids/data', + 'test-asset-1', + '{ + "asset:prop:id": "test-asset-1" + }', + '2019-01-01 00:00:00', + '2019-01-01 00:00:00'), + ('https://my-connector.com/ids/data', + 'test-asset-2', + '{ + "asset:prop:id": "urn:artifact:db-rail-network-2023-jan", + "asset:prop:name": "Rail Network DB 2023 January", + "asset:prop:version": "1.1", + "asset:prop:originator": "https://example-connector.rail-mgmt.bahn.de/api/v1/ids/data", + "asset:prop:originatorOrganization": "Deutsche Bahn AG", + "asset:prop:keywords": "db, bahn, rail, Rail-Designer", + "asset:prop:contenttype": "application/json", + "asset:prop:description": "Train Network Map released on 10.01.2023, valid until 31.02.2023. \nFile format is xyz as exported by Rail-Designer.", + "asset:prop:language": "https://w3id.org/idsa/code/EN", + "asset:prop:publisher": "https://my.cool-api.gg/about", + "asset:prop:standardLicense": "https://my.cool-api.gg/license", + "asset:prop:endpointDocumentation": "https://my.cool-api.gg/docs", + "http://w3id.org/mds#dataCategory": "Infrastructure and Logistics", + "http://w3id.org/mds#dataSubcategory": "General Information About Planning Of Routes", + "http://w3id.org/mds#dataModel": "my-data-model-001", + "http://w3id.org/mds#geoReferenceMethod": "my-geo-reference-method", + "http://w3id.org/mds#transportMode": "Rail" + }', + '2019-01-01 00:00:00', + '2019-01-01 00:00:00'); + +insert into data_offer_contract_offer (contract_offer_id, connector_endpoint, asset_id, policy, created_at, updated_at) +values ('test-contract-offer-1', + 'https://my-connector.com/ids/data', + 'test-asset-1', + '"test-policy-1"', + '2019-01-01 00:00:00', + '2019-01-01 00:00:00'), + ('test-contract-offer-2', + 'https://my-connector.com/ids/data', + 'test-asset-2', + '"test-policy-2"', + '2019-01-01 00:00:00', + '2019-01-01 00:00:00'); + +insert into broker_event_log (created_at, user_message, event, event_status, connector_endpoint, asset_id, error_stack, + duration_in_ms) +values ('2019-01-01 00:00:00', + 'Connector was successfully updated, and changes were incorporated', + 'CONNECTOR_UPDATED', + 'OK', + 'https://my-connector.com/ids/data', + 'test-asset-1', + null, + 100); + +insert into broker_execution_time_measurement (connector_endpoint, created_at, type, error_status, duration_in_ms) +values ('https://my-connector.com/ids/data', + '2019-01-01 00:00:00', + 'CONNECTOR_REFRESH', + 'OK', + 100); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 7355790b8..749777133 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -45,6 +45,9 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String MAX_CONTRACT_OFFERS_PER_CONNECTOR = "edc.broker.server.max.contract.offers.per.connector"; + @Setting + public static final String CATALOG_PAGE_PAGE_SIZE = "edc.broker.server.catalog.page.page.size"; + @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index ff9f24ab5..039ed041d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -161,7 +161,8 @@ public static BrokerServerExtensionContext buildContext( catalogQueryService, policyDtoBuilder, assetPropertyParser, - catalogFilterService + catalogFilterService, + brokerServerSettings ); var connectorApiService = new ConnectorApiService( connectorPageQueryService, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 3fa99ad16..169949527 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -20,8 +20,12 @@ import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageResult; import lombok.RequiredArgsConstructor; @@ -43,4 +47,14 @@ public CatalogPageResult catalogPage(CatalogPageQuery query) { public ConnectorPageResult connectorPage(ConnectorPageQuery query) { return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorPage(dsl, query)); } + + @Override + public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery dataOfferDetailPageQuery) { + throw new IllegalStateException("Not yet implemented!"); + } + + @Override + public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery connectorDetailPageQuery) { + throw new IllegalStateException("Not yet implemented!"); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java index b0c07dc34..7554f21c3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -16,10 +16,14 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; import org.jooq.Field; +import org.jooq.Record; +import org.jooq.SelectOnConditionStep; +import org.jooq.SelectSelectStep; import org.jooq.impl.DSL; import java.util.List; @@ -33,30 +37,56 @@ public class CatalogQueryDataOfferFetcher { /** * Query data offers * - * @param fields query fields - * @param filter filter - * @param sorting sorting + * @param fields query fields + * @param filter filter + * @param sorting sorting + * @param pageQuery pagination * @return {@link Field} of {@link DataOfferRs}s */ - public Field> queryDataOffers(CatalogQueryFields fields, CatalogQueryFilter filter, CatalogPageSortingType sorting) { + public Field> queryDataOffers( + CatalogQueryFields fields, + CatalogQueryFilter filter, + CatalogPageSortingType sorting, + PageQuery pageQuery + ) { var c = fields.getConnectorTable(); var d = fields.getDataOfferTable(); - var query = DSL.select( - fields.getAssetId().as("assetId"), - d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), - d.CREATED_AT, - d.UPDATED_AT, - catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), - c.ENDPOINT.as("connectorEndpoint"), - c.ONLINE_STATUS.as("connectorOnlineStatus"), - fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt") - ) - .from(d) - .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) + var select = DSL.select( + fields.getAssetId().as("assetId"), + d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), + d.CREATED_AT, + d.UPDATED_AT, + catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), + c.ENDPOINT.as("connectorEndpoint"), + c.ONLINE_STATUS.as("connectorOnlineStatus"), + fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt") + ); + + var query = from(select, fields) .where(catalogQueryFilterService.filter(fields, filter)) - .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)); + .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)) + .limit(pageQuery.offset(), pageQuery.limit()); return MultisetUtils.multiset(query, DataOfferRs.class); } + + /** + * Query number of data offers + * + * @param fields query fields + * @param filter filter + * @return {@link Field} with number of data offers + */ + public Field queryNumDataOffers(CatalogQueryFields fields, CatalogQueryFilter filter) { + var query = from(DSL.select(DSL.count()), fields) + .where(catalogQueryFilterService.filter(fields, filter)); + return DSL.field(query); + } + + private SelectOnConditionStep from(SelectSelectStep select, CatalogQueryFields fields) { + var c = fields.getConnectorTable(); + var d = fields.getDataOfferTable(); + return select.from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index 4ee1e45c2..d379558a1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -39,7 +39,7 @@ public class CatalogQueryFields { // Asset Properties from JSON to be used in sorting / filtering Field assetId; - Field assetTitle; + Field assetName; Field assetDescription; Field assetKeywords; @@ -51,7 +51,7 @@ public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable) { this.connectorTable = connectorTable; this.dataOfferTable = dataOfferTable; assetId = dataOfferTable.ASSET_ID; - assetTitle = DSL.coalesce(getAssetProperty(AssetProperty.ASSET_NAME), assetId); + assetName = dataOfferTable.ASSET_NAME; assetDescription = getAssetProperty(AssetProperty.DESCRIPTION); assetKeywords = getAssetProperty(AssetProperty.KEYWORDS); offlineSinceOrLastUpdatedAt = DSL.coalesce( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java index a70c92ae8..5c83e405f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java @@ -36,7 +36,7 @@ public Condition filter(CatalogQueryFields fields, CatalogQueryFilter filter) { var conditions = new ArrayList(); conditions.add(SearchUtils.simpleSearch(filter.searchQuery(), List.of( fields.getAssetId(), - fields.getAssetTitle(), + fields.getAssetName(), fields.getAssetDescription(), fields.getAssetKeywords(), fields.getConnectorTable().ENDPOINT diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java index 8a96763d7..b703f9222 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java @@ -17,6 +17,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogPageRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; @@ -35,6 +36,7 @@ public class CatalogQueryService { * @param dsl transaction * @param filter filter * @param sorting sorting + * @param pageQuery pagination * @param availableFilterValueQueries available filter value queries * @return {@link CatalogPageRs} */ @@ -42,6 +44,7 @@ public CatalogPageRs queryCatalogPage( DSLContext dsl, CatalogQueryFilter filter, CatalogPageSortingType sorting, + PageQuery pageQuery, List availableFilterValueQueries ) { var fields = new CatalogQueryFields(Tables.CONNECTOR, Tables.DATA_OFFER); @@ -49,11 +52,14 @@ public CatalogPageRs queryCatalogPage( var availableFilterValues = catalogQueryAvailableFilterFetcher .queryAvailableFilterValues(fields, filter, availableFilterValueQueries); - var dataOffers = catalogQueryDataOfferFetcher.queryDataOffers(fields, filter, sorting); + var dataOffers = catalogQueryDataOfferFetcher.queryDataOffers(fields, filter, sorting, pageQuery); + + var numTotalDataOffers = catalogQueryDataOfferFetcher.queryNumDataOffers(fields, filter); return dsl.select( dataOffers.as("dataOffers"), - availableFilterValues.as("availableFilterValues") + availableFilterValues.as("availableFilterValues"), + numTotalDataOffers.as("numTotalDataOffers") ).fetchOneInto(CatalogPageRs.class); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java index 3cc92a1d6..a95fe3db6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java @@ -28,7 +28,7 @@ public List> getOrderBy(CatalogQueryFields fields, CatalogPageSort List> orderBy; if (sorting == null || sorting == CatalogPageSortingType.TITLE) { orderBy = List.of( - fields.getAssetTitle().asc(), + fields.getAssetName().asc(), fields.getConnectorTable().ENDPOINT.asc() ); } else if (sorting == CatalogPageSortingType.MOST_RECENT) { @@ -39,7 +39,7 @@ public List> getOrderBy(CatalogQueryFields fields, CatalogPageSort } else if (sorting == CatalogPageSortingType.ORIGINATOR) { orderBy = List.of( fields.getConnectorTable().ENDPOINT.asc(), - fields.getAssetTitle().asc() + fields.getAssetName().asc() ); } else { throw new IllegalArgumentException("Unknown %s: %s".formatted(CatalogPageSortingType.class.getName(), sorting)); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java index d20d950a7..8a7e7f9c9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java @@ -27,4 +27,5 @@ public class CatalogPageRs { String availableFilterValues; List dataOffers; + int numTotalDataOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java new file mode 100644 index 000000000..c5cd659bb --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; + +public record PageQuery(int offset, int limit) { +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java index 3d45e0b64..47471d9c1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java @@ -28,9 +28,13 @@ public class BrokerServerSettings { @Getter private final Duration hideOfflineDataOffersAfter; + @Getter + private final int catalogPagePageSize; + public BrokerServerSettings(Config config) { this.config = config; - this.hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); + hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); + catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); } private Duration getDurationOrNull(@NonNull String configProperty) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index cc65abf06..21ca02e07 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; +import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; @@ -40,6 +41,7 @@ public class CatalogApiService { private final PolicyDtoBuilder policyDtoBuilder; private final AssetPropertyParser assetPropertyParser; private final CatalogFilterService catalogFilterService; + private final BrokerServerSettings brokerServerSettings; public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { Objects.requireNonNull(query, "query must not be null"); @@ -50,16 +52,30 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { catalogFilterService.getSelectedFiltersQuery(query.getFilter()) ); + var pageQuery = paginationMetadataUtils.getPageQuery( + query.getPageOneBased(), + brokerServerSettings.getCatalogPagePageSize() + ); + + // execute db query var catalogPageRs = catalogQueryService.queryCatalogPage( dsl, filter, query.getSorting(), + pageQuery, catalogFilterService.getAvailableFiltersQuery() ); + var paginationMetadata = paginationMetadataUtils.buildPaginationMetadata( + query.getPageOneBased(), + brokerServerSettings.getCatalogPagePageSize(), + catalogPageRs.getDataOffers().size(), + catalogPageRs.getNumTotalDataOffers() + ); + var result = new CatalogPageResult(); result.setAvailableSortings(buildAvailableSortings()); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(catalogPageRs.getDataOffers().size())); + result.setPaginationMetadata(paginationMetadata); result.setAvailableFilters(catalogFilterService.buildAvailableFilters(catalogPageRs.getAvailableFilterValues())); result.setDataOffers(buildDataOfferListEntries(catalogPageRs.getDataOffers())); return result; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java index 8d20e3701..e88a80fcb 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.PaginationMetadata; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; @@ -25,4 +26,24 @@ public class PaginationMetadataUtils { public PaginationMetadata buildDummyPaginationMetadata(int numResults) { return new PaginationMetadata(numResults, numResults, 1, numResults); } + + public PageQuery getPageQuery(Integer pageOneBased, int pageSize) { + int pageZeroBased = getPageZeroBased(pageOneBased); + int offset = pageZeroBased * pageSize; + return new PageQuery(offset, pageSize); + } + + public PaginationMetadata buildPaginationMetadata(Integer pageOneBased, int pageSize, int numVisible, int numTotalResults) { + int pageZeroBased = getPageZeroBased(pageOneBased); + var paginationMetadata = new PaginationMetadata(); + paginationMetadata.setNumTotal(numTotalResults); + paginationMetadata.setNumVisible(numVisible); + paginationMetadata.setPageOneBased(pageZeroBased + 1); + paginationMetadata.setPageSize(pageSize); + return paginationMetadata; + } + + private int getPageZeroBased(Integer pageOneBased) { + return pageOneBased == null ? 0 : (pageOneBased - 1); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java index 4e5e7747e..a7efa55a0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java @@ -52,6 +52,7 @@ public Collection deduplicateContractOffers(Collection offers) { var dataOffer = new FetchedDataOffer(); dataOffer.setAssetId(asset.getId()); + dataOffer.setAssetName(getAssetName(asset)); dataOffer.setAssetPropertiesJson(getAssetPropertiesJson(asset)); dataOffer.setContractOffers(buildFetchedDataOfferContractOffers(offers)); return dataOffer; @@ -77,6 +78,14 @@ private Collection> groupByAssetId(Collection return contractOffers.stream().collect(groupingBy(offer -> offer.getAsset().getId())).values(); } + private String getAssetName(Asset asset) { + String assetName = asset.getName(); + if (assetName == null) { + assetName = asset.getId(); + } + return assetName; + } + @NotNull @SneakyThrows private String getAssetPropertiesJson(Asset asset) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java index 283e8563c..2676ffb46 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java @@ -56,6 +56,13 @@ public DataOfferRecord newDataOffer(String connectorEndpoint, FetchedDataOffer f * @return whether any fields were updated */ public boolean updateDataOffer(DataOfferRecord dataOffer, FetchedDataOffer fetchedDataOffer, boolean changed) { + if (!Objects.equals(fetchedDataOffer.getAssetName(), dataOffer.getAssetName())) { + Objects.requireNonNull(fetchedDataOffer.getAssetName(), + "Fetched data offer's asset name should have been set as id if name isn't present"); + dataOffer.setAssetName(fetchedDataOffer.getAssetName()); + changed = true; + } + String existingAssetProps = JsonbUtils.getDataOrNull(dataOffer.getAssetProperties()); var fetchedAssetProps = fetchedDataOffer.getAssetPropertiesJson(); if (!Objects.equals(fetchedAssetProps, existingAssetProps)) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java index 5391e455d..78cf0c9a1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java @@ -26,6 +26,7 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedDataOffer { String assetId; + String assetName; String assetPropertiesJson; List contractOffers; } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index d837f82b2..a2589ecd9 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -21,6 +21,8 @@ import de.sovity.edc.client.gen.model.CnfFilterItem; import de.sovity.edc.client.gen.model.CnfFilterValue; import de.sovity.edc.client.gen.model.CnfFilterValueAttribute; +import de.sovity.edc.client.gen.model.DataOfferListEntry; +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; @@ -42,6 +44,7 @@ import java.time.OffsetDateTime; import java.util.List; import java.util.Map; +import java.util.stream.IntStream; import static de.sovity.edc.client.gen.model.DataOfferListEntry.ConnectorOnlineStatusEnum.ONLINE; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; @@ -57,7 +60,9 @@ class CatalogApiTest { @BeforeEach void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( + BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10" + ))); } @Test @@ -198,9 +203,74 @@ void testAvailableFilters_withFilter() { }); } + @Test + void testPagination_firstPage() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today); + IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) + ))); + IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) + ))); + + + var query = new CatalogPageQuery(); + query.setSearchQuery("my-asset"); + query.setSorting(CatalogPageQuery.SortingEnum.TITLE); + + var result = edcClient().brokerServerApi().catalogPage(query); + assertThat(result.getDataOffers()).extracting(DataOfferListEntry::getAssetId) + .isEqualTo(IntStream.range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + + var actual = result.getPaginationMetadata(); + assertThat(actual.getPageOneBased()).isEqualTo(1); + assertThat(actual.getPageSize()).isEqualTo(10); + assertThat(actual.getNumVisible()).isEqualTo(10); + assertThat(actual.getNumTotal()).isEqualTo(15); + }); + } + + @Test + void testPagination_secondPage() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today); + IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) + ))); + IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) + ))); + + + var query = new CatalogPageQuery(); + query.setSearchQuery("my-asset"); + query.setPageOneBased(2); + query.setSorting(CatalogPageQuery.SortingEnum.TITLE); + + var result = edcClient().brokerServerApi().catalogPage(query); + + assertThat(result.getDataOffers()).extracting(DataOfferListEntry::getAssetId) + .isEqualTo(IntStream.range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + + var actual = result.getPaginationMetadata(); + assertThat(actual.getPageOneBased()).isEqualTo(2); + assertThat(actual.getPageSize()).isEqualTo(10); + assertThat(actual.getNumVisible()).isEqualTo(5); + assertThat(actual.getNumTotal()).isEqualTo(15); + }); + } + private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); dataOffer.setConnectorEndpoint("http://my-connector/ids/data"); dataOffer.setCreatedAt(today.minusDays(5)); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java index 601e66db0..57ae8e571 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java @@ -29,6 +29,7 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; @@ -75,29 +76,33 @@ public void initialize(DSLContext dsl) { } private DataOfferContractOfferRecord dummyContractOffer(Do dataOffer, Co contractOffer) { - return new DataOfferContractOfferRecord( - contractOffer.getId(), - connectorEndpoint, - dataOffer.getAssetId(), - JSONB.valueOf(dummyPolicyJson(contractOffer.getPolicyValue())), - old, - old - ); + var contractOfferRecord = new DataOfferContractOfferRecord(); + contractOfferRecord.setConnectorEndpoint(connectorEndpoint); + contractOfferRecord.setAssetId(dataOffer.getAssetId()); + contractOfferRecord.setContractOfferId(contractOffer.getId()); + contractOfferRecord.setPolicy(JSONB.valueOf(dummyPolicyJson(contractOffer.getPolicyValue()))); + contractOfferRecord.setCreatedAt(old); + contractOfferRecord.setUpdatedAt(old); + return contractOfferRecord; } private DataOfferRecord dummyDataOffer(Do dataOffer) { - return new DataOfferRecord( - connectorEndpoint, - dataOffer.getAssetId(), - JSONB.valueOf(dummyAssetJson(dataOffer)), - old, - old - ); + var assetName = Optional.of(dataOffer.getAssetName()).orElse(dataOffer.getAssetId()); + + var dataOfferRecord = new DataOfferRecord(); + dataOfferRecord.setConnectorEndpoint(connectorEndpoint); + dataOfferRecord.setAssetId(dataOffer.getAssetId()); + dataOfferRecord.setAssetName(assetName); + dataOfferRecord.setAssetProperties(JSONB.valueOf(dummyAssetJson(dataOffer))); + dataOfferRecord.setCreatedAt(old); + dataOfferRecord.setUpdatedAt(old); + return dataOfferRecord; } private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { var fetchedDataOffer = new FetchedDataOffer(); fetchedDataOffer.setAssetId(dataOffer.getAssetId()); + fetchedDataOffer.setAssetName(dataOffer.getAssetName()); fetchedDataOffer.setAssetPropertiesJson(dummyAssetJson(dataOffer)); var contractOffersMapped = dataOffer.getContractOffers().stream().map(this::dummyFetchedContractOffer).collect(Collectors.toList()); From c23e71a450a1c5b2523647034542cc857d603c67 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:03:50 +0200 Subject: [PATCH 059/295] feat: dataspace filter (#130) * feat: dataspace filter * test: add dataspace filter * chore: revert dataspace as asset prop * feat: add dataSpace to CatalogQueryFields * feat: add dataSpace to CatalogQueryFiaelds * feat: add dataSpace to CatalogQueryFields * feat: add dataSpace to CatalogQueryFields * chore: fix checkstyle * refactor: add DataSpaceConfig * refactor: buildDataSpaceField * refactor: BrokerServerSettings * feat: get known dataspaces from config * refactor: PR remarks * refactor: minor refactorings * refactor: minor refactorings * refactor: minor refactorings * refactor: minor refactorings * refactor: minor refactorings * refactor: minor refactorings * refactor: minor refactorings * test: test_available_filter_values_to_filter_by * test: test_available_filter_values_to_filter_by --- connector/.env | 2 + extensions/broker-server/build.gradle.kts | 1 + .../brokerserver/BrokerServerExtension.java | 6 + .../BrokerServerExtensionContextBuilder.java | 5 +- .../dao/pages/catalog/CatalogQueryFields.java | 24 +++- .../catalog/CatalogQueryFilterService.java | 2 +- .../pages/catalog/CatalogQueryService.java | 6 +- .../services/BrokerServerSettings.java | 48 -------- .../services/api/CatalogApiService.java | 5 +- ...talogFilterAttributeDefinitionService.java | 9 ++ .../api/filtering/CatalogFilterService.java | 1 + .../services/config/BrokerServerSettings.java | 30 +++++ .../config/BrokerServerSettingsFactory.java | 83 +++++++++++++ .../services/config/DataSpaceConfig.java | 20 ++++ .../services/config/DataSpaceConnector.java | 23 ++++ .../services/api/CatalogApiTest.java | 111 ++++++++++++++---- .../offers/DataOfferWriterTestDydi.java | 4 +- 17 files changed, 300 insertions(+), 80 deletions(-) delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java diff --git a/connector/.env b/connector/.env index a853dcd2b..dada64602 100644 --- a/connector/.env +++ b/connector/.env @@ -33,6 +33,8 @@ EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_CONNECTOR=10 # Pagination Defaults EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 +EDC_BROKER_SERVER_DEFAULT_DATASPACE=MDS +EDC_BROKER_SERVER_KNOWN_DATASPACES_ENDPOINTS=Example1=http://connector-endpoint1.org;Example2=http://connector-endpoint2.org # =========================================================== # Other EDC Config diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 0067b577c..079fc4b7f 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { testCompileOnly("org.projectlombok:lombok:1.18.28") testImplementation("org.assertj:assertj-core:${assertj}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.mockito:mockito-inline:${mockitoVersion}") testImplementation("${edcGroup}:control-plane-core:${edcVersion}") testImplementation("${edcGroup}:junit:${edcVersion}") testImplementation("${edcGroup}:http:${edcVersion}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 749777133..abc2fa2cf 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -48,6 +48,12 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String CATALOG_PAGE_PAGE_SIZE = "edc.broker.server.catalog.page.page.size"; + @Setting + public static final String DEFAULT_CONNECTOR_DATASPACE = "edc.broker.server.default.dataspace"; + + @Setting + public static final String KNOWN_DATASPACES_ENDPOINTS = "edc.broker.server.known.dataspaces.endpoints"; + @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 039ed041d..4cfdeaee7 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -27,7 +27,6 @@ import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; -import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; @@ -37,6 +36,7 @@ import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterAttributeDefinitionService; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettingsFactory; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; @@ -84,7 +84,8 @@ public static BrokerServerExtensionContext buildContext( TypeManager typeManager, CatalogService catalogService ) { - var brokerServerSettings = new BrokerServerSettings(config); + var brokerServerSettingsFactory = new BrokerServerSettingsFactory(); + var brokerServerSettings = brokerServerSettingsFactory.buildBrokerServerSettings(config); // Dao var dataOfferQueries = new DataOfferQueries(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index d379558a1..3b6710e90 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; +import de.sovity.edc.ext.brokerserver.services.config.DataSpaceConfig; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.FieldDefaults; @@ -42,12 +43,13 @@ public class CatalogQueryFields { Field assetName; Field assetDescription; Field assetKeywords; + Field dataSpace; // This date should always be non-null // It's used in the UI to display the last relevant change date of a connector Field offlineSinceOrLastUpdatedAt; - public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable) { + public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable, DataSpaceConfig dataSpaceConfig) { this.connectorTable = connectorTable; this.dataOfferTable = dataOfferTable; assetId = dataOfferTable.ASSET_ID; @@ -58,6 +60,26 @@ public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable) { connectorTable.LAST_SUCCESSFUL_REFRESH_AT, connectorTable.CREATED_AT ); + + dataSpace = buildDataSpaceField(connectorTable, dataSpaceConfig); + } + + private Field buildDataSpaceField(Connector connectorTable, DataSpaceConfig dataSpaceConfig) { + var endpoint = connectorTable.ENDPOINT; + + var connectors = dataSpaceConfig.dataSpaceConnectors(); + if (connectors.isEmpty()) { + return DSL.val(dataSpaceConfig.defaultDataSpace()); + } + + var first = connectors.get(0); + var dspCase = DSL.case_(endpoint).when(first.endpoint(), first.dataSpaceName()); + + for (var dsp : connectors.subList(1, connectors.size())) { + dspCase = dspCase.when(dsp.endpoint(), dsp.dataSpaceName()); + } + + return dspCase.else_(DSL.val(dataSpaceConfig.defaultDataSpace())); } public Field getAssetProperty(String name) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java index 5c83e405f..6d2d15e71 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java @@ -18,7 +18,7 @@ import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jooq.Condition; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java index b703f9222..e09f18b1b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java @@ -19,6 +19,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.services.config.DataSpaceConfig; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -45,9 +46,10 @@ public CatalogPageRs queryCatalogPage( CatalogQueryFilter filter, CatalogPageSortingType sorting, PageQuery pageQuery, - List availableFilterValueQueries + List availableFilterValueQueries, + DataSpaceConfig dataSpaceConfig ) { - var fields = new CatalogQueryFields(Tables.CONNECTOR, Tables.DATA_OFFER); + var fields = new CatalogQueryFields(Tables.CONNECTOR, Tables.DATA_OFFER, dataSpaceConfig); var availableFilterValues = catalogQueryAvailableFilterFetcher .queryAvailableFilterValues(fields, filter, availableFilterValueQueries); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java deleted file mode 100644 index 47471d9c1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerSettings.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import lombok.Getter; -import lombok.NonNull; -import org.apache.commons.lang3.StringUtils; -import org.eclipse.edc.spi.system.configuration.Config; - -import java.time.Duration; - -public class BrokerServerSettings { - private final Config config; - - @Getter - private final Duration hideOfflineDataOffersAfter; - - @Getter - private final int catalogPagePageSize; - - public BrokerServerSettings(Config config) { - this.config = config; - hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); - catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); - } - - private Duration getDurationOrNull(@NonNull String configProperty) { - var durationAsString = config.getString(configProperty, ""); - if (StringUtils.isBlank(durationAsString)) { - return null; - } - - return Duration.parse(durationAsString); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index 21ca02e07..f05077a39 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -18,7 +18,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; -import de.sovity.edc.ext.brokerserver.services.BrokerServerSettings; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; @@ -63,7 +63,8 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { filter, query.getSorting(), pageQuery, - catalogFilterService.getAvailableFiltersQuery() + catalogFilterService.getAvailableFiltersQuery(), + brokerServerSettings.getDataSpaceConfig() ); var paginationMetadata = paginationMetadataUtils.buildPaginationMetadata( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java index 3b4cbed31..c1ce78afc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java @@ -31,6 +31,15 @@ public CatalogFilterAttributeDefinition fromAssetProperty(String assetProperty, ); } + public CatalogFilterAttributeDefinition buildDataSpaceFilter() { + return new CatalogFilterAttributeDefinition( + "dataSpace", + "Data Space", + CatalogQueryFields::getDataSpace, + (fields, values) -> PostgresqlUtils.in(fields.getDataSpace(), values) + ); + } + @NotNull private Field getValue(CatalogQueryFields fields, String assetProperty) { return DSL.coalesce(fields.getAssetProperty(assetProperty), DSL.value("")); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index 266724354..c5b4e2175 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -58,6 +58,7 @@ public class CatalogFilterService { */ private List getAvailableFilters() { return List.of( + catalogFilterAttributeDefinitionService.buildDataSpaceFilter(), catalogFilterAttributeDefinitionService.fromAssetProperty( AssetProperty.DATA_CATEGORY, "Data Category" diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java new file mode 100644 index 000000000..d5373ea71 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.config; + +import lombok.Builder; +import lombok.Value; + +import java.time.Duration; + +@Value +@Builder +public class BrokerServerSettings { + Duration hideOfflineDataOffersAfter; + + int catalogPagePageSize; + + DataSpaceConfig dataSpaceConfig; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java new file mode 100644 index 000000000..eab79eb00 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.config; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class BrokerServerSettingsFactory { + private Config config; + + public BrokerServerSettings buildBrokerServerSettings(Config config) { + this.config = config; + + var hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); + var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); + var dataSpaceConfig = buildDataSpaceConfig(config); + + return BrokerServerSettings.builder() + .hideOfflineDataOffersAfter(hideOfflineDataOffersAfter) + .catalogPagePageSize(catalogPagePageSize) + .dataSpaceConfig(dataSpaceConfig) + .build(); + } + + private DataSpaceConfig buildDataSpaceConfig(Config config) { + return new DataSpaceConfig(getKnownDataSpaceEndpoints(config), getDefaultDataSpace(config)); + } + + private List getKnownDataSpaceEndpoints(Config config) { + // Example: "Example1=http://connector-endpoint1.org;Example2=http://connector-endpoint2.org" + var defaultDataSpaces = new ArrayList(); + var dataSpacesConfig = config.getString(BrokerServerExtension.KNOWN_DATASPACES_ENDPOINTS, ""); + + var allDataSpaces = dataSpacesConfig.split(";"); + for (var dataSpace : allDataSpaces) { + var dataSpaceParts = dataSpace.split("="); + if (dataSpaceParts.length != 2) { + continue; + } + + var dataSpaceName = dataSpaceParts[0].trim(); + var dataSpaceEndpoint = dataSpaceParts[1].trim(); + if (StringUtils.isBlank(dataSpaceName) || StringUtils.isBlank(dataSpaceEndpoint)) { + continue; + } + + defaultDataSpaces.add(new DataSpaceConnector(dataSpaceEndpoint, dataSpaceName)); + } + + return defaultDataSpaces; + } + + private String getDefaultDataSpace(Config config) { + return config.getString(BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "Default"); + } + + private Duration getDurationOrNull(@NonNull String configProperty) { + var durationAsString = config.getString(configProperty, ""); + if (StringUtils.isBlank(durationAsString)) { + return null; + } + + return Duration.parse(durationAsString); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java new file mode 100644 index 000000000..705a5c1cc --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.config; + +import java.util.List; + +public record DataSpaceConfig(List dataSpaceConnectors, String defaultDataSpace) { +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java new file mode 100644 index 000000000..5e8d57cf3 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + + +package de.sovity.edc.ext.brokerserver.services.config; + +/** + * We have special connectors that represent entire other data spaces. + * Here we associate the name of the data space with the connector endpoint. + */ +public record DataSpaceConnector(String endpoint, String dataSpaceName) { +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index a2589ecd9..56a7f4062 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -61,21 +61,86 @@ class CatalogApiTest { @BeforeEach void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10" + BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", + BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", + BrokerServerExtension.KNOWN_DATASPACES_ENDPOINTS, "Example1=http://my-connector/ids/data" ))); } + @Test + void testDataSpace_two_dataspaces_filter_for_one() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: Example1 + createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: MDS + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" + ), "http://my-connector/ids/data"); // Dataspace: Example1 + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" + ), "http://my-connector2/ids/data"); // Dataspace: MDS + + var query = new CatalogPageQuery(); + query.setFilter(new CnfFilterValue(List.of( + new CnfFilterValueAttribute("dataSpace", List.of("MDS")) + ))); + + var result = edcClient().brokerServerApi().catalogPage(query); + assertThat(result.getDataOffers()).hasSize(1); + + var dataOfferResult = result.getDataOffers().get(0); + assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("http://my-connector2/ids/data"); + }); + } + + @Test + void test_available_filter_values_to_filter_by() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: Example1 + createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: MDS + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "de" + ), "http://my-connector/ids/data"); // Dataspace: Example1 + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "en" + ), "http://my-connector2/ids/data"); // Dataspace: MDS + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset2", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" + ), "http://my-connector2/ids/data"); // Dataspace: MDS + + // get all available filter values + var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()).getAvailableFilters(); + + // assert that the filter values are correct + var dataSpace = getAvailableFilter(edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()), "dataSpace"); + assertThat(dataSpace.getValues()).containsExactly(new CnfFilterItem("Example1", "Example1"), new CnfFilterItem("MDS", "MDS")); + }); + } + @Test void testDataOfferDetails() { TEST_DATABASE.testTransaction(dsl -> { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today); + createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset", AssetProperty.ASSET_NAME, "my-asset" - )); + ), "http://my-connector/ids/data"); var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -101,30 +166,30 @@ void testAvailableFilters_noFilter() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today); + createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", AssetProperty.DATA_CATEGORY, "my-category-1", AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" - )); + ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", AssetProperty.DATA_CATEGORY, "my-category-1", AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" - )); + ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", AssetProperty.DATA_CATEGORY, "my-category-1", AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" - )); + ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", AssetProperty.DATA_CATEGORY, "my-category-1", AssetProperty.TRANSPORT_MODE, "" - )); + ), "http://my-connector/ids/data"); var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -132,6 +197,7 @@ void testAvailableFilters_noFilter() { assertThat(result.getAvailableFilters().getFields()) .extracting(CnfFilterAttribute::getId) .containsExactly( + "dataSpace", AssetProperty.DATA_CATEGORY, AssetProperty.DATA_SUBCATEGORY, AssetProperty.DATA_MODEL, @@ -142,6 +208,7 @@ void testAvailableFilters_noFilter() { assertThat(result.getAvailableFilters().getFields()) .extracting(CnfFilterAttribute::getTitle) .containsExactly( + "Data Space", "Data Category", "Data Subcategory", "Data Model", @@ -178,14 +245,14 @@ void testAvailableFilters_withFilter() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today); + createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", AssetProperty.DATA_CATEGORY, "my-category" - )); + ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-2" - )); + ), "http://my-connector/ids/data"); var query = new CatalogPageQuery(); @@ -209,13 +276,13 @@ void testPagination_firstPage() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today); + createConnector(dsl, today, "http://my-connector/ids/data"); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) - ))); + ), "http://my-connector/ids/data")); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) - ))); + ), "http://my-connector/ids/data")); var query = new CatalogPageQuery(); @@ -240,13 +307,13 @@ void testPagination_secondPage() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today); + createConnector(dsl, today, "http://my-connector/ids/data"); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) - ))); + ), "http://my-connector/ids/data")); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) - ))); + ), "http://my-connector/ids/data")); var query = new CatalogPageQuery(); @@ -267,19 +334,19 @@ void testPagination_secondPage() { }); } - private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties) { + private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); - dataOffer.setConnectorEndpoint("http://my-connector/ids/data"); + dataOffer.setConnectorEndpoint(connectorEndpoint); dataOffer.setCreatedAt(today.minusDays(5)); dataOffer.setUpdatedAt(today); dataOffer.insert(); var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint("http://my-connector/ids/data"); + contractOffer.setConnectorEndpoint(connectorEndpoint); contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); contractOffer.setCreatedAt(today.minusDays(5)); contractOffer.setUpdatedAt(today); @@ -287,10 +354,10 @@ private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map Date: Wed, 21 Jun 2023 13:20:32 +0200 Subject: [PATCH 060/295] fix: filter should not be applied to itself when fetching possible values, but other filters should (#133) --- CHANGELOG.md | 2 + .../BrokerServerExtensionContextBuilder.java | 30 ++++++++-- .../CatalogQueryAvailableFilterFetcher.java | 44 +++++++++----- .../catalog/CatalogQueryDataOfferFetcher.java | 23 +++---- .../dao/pages/catalog/CatalogQueryFields.java | 11 ++++ .../catalog/CatalogQueryFilterService.java | 11 +++- .../pages/catalog/CatalogQueryService.java | 33 +++++----- .../catalog/models/CatalogQueryFilter.java | 8 +-- .../services/api/CatalogApiService.java | 15 ++--- .../api/filtering/CatalogFilterService.java | 50 ++++++++-------- .../brokerserver/utils/CollectionUtils2.java | 8 +++ .../services/api/CatalogApiTest.java | 52 +++++++++------- .../utils/CollectionUtils2Test.java | 60 +++++++++++++++++++ 13 files changed, 238 insertions(+), 109 deletions(-) create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b7faf41a7..54cf27269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Patch +- Fix: Data Offer Filter available values are no longer limited to the selected value if a value is selected. + ## [v0.0.1] Broker PoC Release - 2023-06-02 Initial Broker PoC Release with a minimalistic feature set. diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 4cfdeaee7..ebedefdef 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -96,9 +96,17 @@ public static BrokerServerExtensionContext buildContext( var catalogQuerySortingService = new CatalogQuerySortingService(); var catalogQueryFilterService = new CatalogQueryFilterService(brokerServerSettings); var catalogQueryContractOfferFetcher = new CatalogQueryContractOfferFetcher(); - var catalogQueryDataOfferFetcher = new CatalogQueryDataOfferFetcher(catalogQuerySortingService, catalogQueryFilterService, catalogQueryContractOfferFetcher); + var catalogQueryDataOfferFetcher = new CatalogQueryDataOfferFetcher( + catalogQuerySortingService, + catalogQueryFilterService, + catalogQueryContractOfferFetcher + ); var catalogQueryAvailableFilterFetcher = new CatalogQueryAvailableFilterFetcher(catalogQueryFilterService); - var catalogQueryService = new CatalogQueryService(catalogQueryDataOfferFetcher, catalogQueryAvailableFilterFetcher); + var catalogQueryService = new CatalogQueryService( + catalogQueryDataOfferFetcher, + catalogQueryAvailableFilterFetcher, + brokerServerSettings + ); var connectorPageQueryService = new ConnectorPageQueryService(); @@ -118,7 +126,11 @@ public static BrokerServerExtensionContext buildContext( ); var dataOfferPatchApplier = new DataOfferPatchApplier(); var dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); - var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter(brokerEventLogger, dataOfferWriter, dataOfferLimitsEnforcer); + var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter( + brokerEventLogger, + dataOfferWriter, + dataOfferLimitsEnforcer + ); var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger, monitor); var contractOfferFetcher = new ContractOfferFetcher(catalogService); var fetchedDataOfferBuilder = new DataOfferBuilder(objectMapper); @@ -139,7 +151,11 @@ public static BrokerServerExtensionContext buildContext( var connectorQueue = new ConnectorQueue(connectorUpdater, threadPool); var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); var connectorCreator = new ConnectorCreator(connectorQueries); - var knownConnectorsInitializer = new KnownConnectorsInitializer(config, connectorQueue, connectorCreator); + var knownConnectorsInitializer = new KnownConnectorsInitializer( + config, + connectorQueue, + connectorCreator + ); var catalogFilterAttributeDefinitionService = new CatalogFilterAttributeDefinitionService(); var catalogFilterService = new CatalogFilterService(catalogFilterAttributeDefinitionService); @@ -154,7 +170,11 @@ public static BrokerServerExtensionContext buildContext( // Startup var quartzScheduleInitializer = new QuartzScheduleInitializer(config, monitor, jobs); - var brokerServerInitializer = new BrokerServerInitializer(dslContextFactory, knownConnectorsInitializer, quartzScheduleInitializer); + var brokerServerInitializer = new BrokerServerInitializer( + dslContextFactory, + knownConnectorsInitializer, + quartzScheduleInitializer + ); // UI Capabilities var catalogApiService = new CatalogApiService( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java index 3df3c2ec0..4e312acc1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java @@ -14,14 +14,14 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import lombok.RequiredArgsConstructor; import org.jooq.Field; import org.jooq.JSON; import org.jooq.impl.DSL; -import org.jooq.util.postgres.PostgresDSL; +import java.util.ArrayList; import java.util.List; @RequiredArgsConstructor @@ -31,27 +31,41 @@ public class CatalogQueryAvailableFilterFetcher { /** * Query available filter values. * - * @param fields query fields - * @param filter general filter to narrow results down to - * @param availableFilterValuesQueries one entry for each filter + * @param fields query fields + * @param searchQuery search query + * @param filters filters (values + filter clauses) * @return {@link Field} with field[iFilter][iValue] */ public Field queryAvailableFilterValues( CatalogQueryFields fields, - CatalogQueryFilter filter, - List availableFilterValuesQueries + String searchQuery, + List filters ) { + List> resultFields = new ArrayList<>(); + for (int i = 0; i < filters.size(); i++) { + // When querying a filter's values we apply all filters except for the current filter's values + var currentFilter = filters.get(i); + var otherFilters = CollectionUtils2.allElementsExceptForIndex(filters, i); + var resultField = queryFilterValues(fields, currentFilter, searchQuery, otherFilters); + resultFields.add(resultField); + } + return DSL.select(DSL.jsonArray(resultFields)).asField(); + } + + private Field queryFilterValues( + CatalogQueryFields parentQueryFields, + CatalogQueryFilter currentFilter, + String searchQuery, + List otherFilters + ) { + var fields = parentQueryFields.withSuffix("filter_" + currentFilter.name()); var c = fields.getConnectorTable(); var d = fields.getDataOfferTable(); - var valuesPerFilterAttribute = availableFilterValuesQueries.stream() - .map(it -> it.getAttributeValueField(fields)) - .map(PostgresDSL::arrayAggDistinct) - .toList(); - - return DSL.select(DSL.jsonArray(valuesPerFilterAttribute)) - .from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) - .where(catalogQueryFilterService.filter(fields, filter)) + return DSL.select(DSL.arrayAggDistinct(currentFilter.valueQuery().getAttributeValueField(fields))) + .from(d) + .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) + .where(catalogQueryFilterService.filter(fields, searchQuery, otherFilters)) .asField(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java index 7554f21c3..7768192ee 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -37,15 +37,17 @@ public class CatalogQueryDataOfferFetcher { /** * Query data offers * - * @param fields query fields - * @param filter filter - * @param sorting sorting - * @param pageQuery pagination + * @param fields query fields + * @param searchQuery search query + * @param filters filters (queries + filter clauses) + * @param sorting sorting + * @param pageQuery pagination * @return {@link Field} of {@link DataOfferRs}s */ public Field> queryDataOffers( CatalogQueryFields fields, - CatalogQueryFilter filter, + String searchQuery, + List filters, CatalogPageSortingType sorting, PageQuery pageQuery ) { @@ -64,7 +66,7 @@ public Field> queryDataOffers( ); var query = from(select, fields) - .where(catalogQueryFilterService.filter(fields, filter)) + .where(catalogQueryFilterService.filter(fields, searchQuery, filters)) .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)) .limit(pageQuery.offset(), pageQuery.limit()); @@ -74,13 +76,14 @@ public Field> queryDataOffers( /** * Query number of data offers * - * @param fields query fields - * @param filter filter + * @param fields query fields + * @param searchQuery search query + * @param filters filters (queries + filter clauses) * @return {@link Field} with number of data offers */ - public Field queryNumDataOffers(CatalogQueryFields fields, CatalogQueryFilter filter) { + public Field queryNumDataOffers(CatalogQueryFields fields, String searchQuery, List filters) { var query = from(DSL.select(DSL.count()), fields) - .where(catalogQueryFilterService.filter(fields, filter)); + .where(catalogQueryFilterService.filter(fields, searchQuery, filters)); return DSL.field(query); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index 3b6710e90..94bf912e7 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -49,9 +49,12 @@ public class CatalogQueryFields { // It's used in the UI to display the last relevant change date of a connector Field offlineSinceOrLastUpdatedAt; + DataSpaceConfig dataSpaceConfig; + public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable, DataSpaceConfig dataSpaceConfig) { this.connectorTable = connectorTable; this.dataOfferTable = dataOfferTable; + this.dataSpaceConfig = dataSpaceConfig; assetId = dataOfferTable.ASSET_ID; assetName = dataOfferTable.ASSET_NAME; assetDescription = getAssetProperty(AssetProperty.DESCRIPTION); @@ -85,4 +88,12 @@ private Field buildDataSpaceField(Connector connectorTable, DataSpaceCon public Field getAssetProperty(String name) { return JsonbDSL.fieldByKeyText(dataOfferTable.ASSET_PROPERTIES, name); } + + public CatalogQueryFields withSuffix(String additionalSuffix) { + return new CatalogQueryFields( + connectorTable.as(connectorTable.getName() + "_" + additionalSuffix), + dataOfferTable.as(dataOfferTable.getName() + "_" + additionalSuffix), + dataSpaceConfig + ); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java index 6d2d15e71..9f83645be 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; @@ -27,22 +28,26 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @RequiredArgsConstructor public class CatalogQueryFilterService { private final BrokerServerSettings brokerServerSettings; - public Condition filter(CatalogQueryFields fields, CatalogQueryFilter filter) { + public Condition filter(CatalogQueryFields fields, String searchQuery, List filters) { var conditions = new ArrayList(); - conditions.add(SearchUtils.simpleSearch(filter.searchQuery(), List.of( + conditions.add(SearchUtils.simpleSearch(searchQuery, List.of( fields.getAssetId(), fields.getAssetName(), + fields.getAssetProperty(AssetProperty.DATA_CATEGORY), + fields.getAssetProperty(AssetProperty.DATA_SUBCATEGORY), fields.getAssetDescription(), fields.getAssetKeywords(), fields.getConnectorTable().ENDPOINT ))); conditions.add(onlyOnlineOrRecentlyOfflineConnectors(fields.getConnectorTable())); - conditions.addAll(filter.selectedFilters().stream().map(it -> it.filterDataOffers(fields)).toList()); + conditions.addAll(filters.stream().map(CatalogQueryFilter::queryFilterClauseOrNull) + .filter(Objects::nonNull).map(it -> it.filterDataOffers(fields)).toList()); return DSL.and(conditions); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java index e09f18b1b..3a57981aa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java @@ -14,12 +14,11 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogPageRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.services.config.DataSpaceConfig; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -30,33 +29,37 @@ public class CatalogQueryService { private final CatalogQueryDataOfferFetcher catalogQueryDataOfferFetcher; private final CatalogQueryAvailableFilterFetcher catalogQueryAvailableFilterFetcher; + private final BrokerServerSettings brokerServerSettings; /** * Query all data required for the catalog page * - * @param dsl transaction - * @param filter filter - * @param sorting sorting - * @param pageQuery pagination - * @param availableFilterValueQueries available filter value queries + * @param dsl transaction + * @param searchQuery search query + * @param filters filters (queries + filter clauses) + * @param sorting sorting + * @param pageQuery pagination * @return {@link CatalogPageRs} */ public CatalogPageRs queryCatalogPage( DSLContext dsl, - CatalogQueryFilter filter, + String searchQuery, + List filters, CatalogPageSortingType sorting, - PageQuery pageQuery, - List availableFilterValueQueries, - DataSpaceConfig dataSpaceConfig + PageQuery pageQuery ) { - var fields = new CatalogQueryFields(Tables.CONNECTOR, Tables.DATA_OFFER, dataSpaceConfig); + var fields = new CatalogQueryFields( + Tables.CONNECTOR, + Tables.DATA_OFFER, + brokerServerSettings.getDataSpaceConfig() + ); var availableFilterValues = catalogQueryAvailableFilterFetcher - .queryAvailableFilterValues(fields, filter, availableFilterValueQueries); + .queryAvailableFilterValues(fields, searchQuery, filters); - var dataOffers = catalogQueryDataOfferFetcher.queryDataOffers(fields, filter, sorting, pageQuery); + var dataOffers = catalogQueryDataOfferFetcher.queryDataOffers(fields, searchQuery, filters, sorting, pageQuery); - var numTotalDataOffers = catalogQueryDataOfferFetcher.queryNumDataOffers(fields, filter); + var numTotalDataOffers = catalogQueryDataOfferFetcher.queryNumDataOffers(fields, searchQuery, filters); return dsl.select( dataOffers.as("dataOffers"), diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java index dc1b41a2a..9dcccd038 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java @@ -14,11 +14,11 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; -import java.util.List; +import lombok.NonNull; public record CatalogQueryFilter( - String searchQuery, - List selectedFilters + @NonNull String name, + @NonNull AvailableFilterValuesQuery valueQuery, + CatalogQuerySelectedFilterQuery queryFilterClauseOrNull ) { - } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index f05077a39..e9061ca9c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -15,11 +15,10 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingItem; @@ -47,10 +46,7 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { Objects.requireNonNull(query, "query must not be null"); - var filter = new CatalogQueryFilter( - query.getSearchQuery(), - catalogFilterService.getSelectedFiltersQuery(query.getFilter()) - ); + var filters = catalogFilterService.getCatalogQueryFilters(query.getFilter()); var pageQuery = paginationMetadataUtils.getPageQuery( query.getPageOneBased(), @@ -60,11 +56,10 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { // execute db query var catalogPageRs = catalogQueryService.queryCatalogPage( dsl, - filter, + query.getSearchQuery(), + filters, query.getSorting(), - pageQuery, - catalogFilterService.getAvailableFiltersQuery(), - brokerServerSettings.getDataSpaceConfig() + pageQuery ); var paginationMetadata = paginationMetadataUtils.buildPaginationMetadata( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index c5b4e2175..c653b041d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -15,11 +15,10 @@ package de.sovity.edc.ext.brokerserver.services.api.filtering; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; -import de.sovity.edc.ext.brokerserver.utils.MapUtils; import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilter; import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterAttribute; import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterItem; @@ -27,14 +26,14 @@ import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterValueAttribute; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.NotNull; -import org.jooq.impl.DSL; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Stream; +import static java.util.stream.Collectors.toMap; + @RequiredArgsConstructor public class CatalogFilterService { private final CatalogFilterAttributeDefinitionService catalogFilterAttributeDefinitionService; @@ -82,8 +81,22 @@ private List getAvailableFilters() { ); } - public List getAvailableFiltersQuery() { - return getAvailableFilters().stream().map(CatalogFilterAttributeDefinition::valueGetter).toList(); + public List getCatalogQueryFilters(CnfFilterValue cnfFilterValue) { + var values = getCnfFilterValuesMap(cnfFilterValue); + return getAvailableFilters().stream() + .map(filter -> new CatalogQueryFilter( + filter.name(), + filter.valueGetter(), + getQueryFilter(filter, values.get(filter.name())) + )) + .toList(); + } + + private CatalogQuerySelectedFilterQuery getQueryFilter(CatalogFilterAttributeDefinition filter, List values) { + if (CollectionUtils2.isNotEmpty(values)) { + return fields -> filter.filterApplier().filterDataOffers(fields, values); + } + return null; } public CnfFilter buildAvailableFilters(String filterValuesJson) { @@ -98,19 +111,6 @@ public CnfFilter buildAvailableFilters(String filterValuesJson) { return new CnfFilter(filterAttributes); } - - public List getSelectedFiltersQuery(CnfFilterValue selectedFilters) { - if (selectedFilters == null || selectedFilters.getSelectedAttributeValues() == null) { - return List.of(); - } - - var availableFilters = MapUtils.associateBy(getAvailableFilters(), CatalogFilterAttributeDefinition::name); - return selectedFilters.getSelectedAttributeValues().stream() - .filter(selectedFilter -> CollectionUtils2.isNotEmpty(selectedFilter.getSelectedIds())) - .map(selectedFilter -> buildSelectedFilter(availableFilters, selectedFilter)) - .toList(); - } - private List buildAvailableFilterValues(AvailableFilter availableFilter) { return availableFilter.availableValues().stream() .sorted(caseInsensitiveEmptyStringLast) @@ -133,12 +133,12 @@ private Stream zipAvailableFilters(List availableValues) { } - @NotNull - private CatalogQuerySelectedFilterQuery buildSelectedFilter(Map availableFilters, CnfFilterValueAttribute selected) { - var available = availableFilters.get(selected.getId()); - if (available == null) { - return fields -> DSL.falseCondition(); + private Map> getCnfFilterValuesMap(CnfFilterValue cnfFilterValue) { + if (cnfFilterValue == null || cnfFilterValue.getSelectedAttributeValues() == null) { + return Map.of(); } - return fields -> available.filterApplier().filterDataOffers(fields, selected.getSelectedIds()); + return cnfFilterValue.getSelectedAttributeValues().stream() + .filter(it -> it.getId() != null && CollectionUtils2.isNotEmpty(it.getSelectedIds())) + .collect(toMap(CnfFilterValueAttribute::getId, CnfFilterValueAttribute::getSelectedIds)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java index e9b8a0b18..ef3337a62 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java @@ -18,8 +18,10 @@ import lombok.NoArgsConstructor; import lombok.NonNull; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -41,4 +43,10 @@ public static Set difference(@NonNull Collection a, @NonNull Collectio public static boolean isNotEmpty(Collection collection) { return collection != null && !collection.isEmpty(); } + + public static List allElementsExceptForIndex(Collection source, int skipIndex) { + var result = new ArrayList<>(source); + result.remove(skipIndex); + return result; + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 56a7f4062..5ef1547b7 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -76,17 +76,17 @@ void testDataSpace_two_dataspaces_filter_for_one() { createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: Example1 createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector2/ids/data"); // Dataspace: MDS var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataSpace", List.of("MDS")) + new CnfFilterValueAttribute("dataSpace", List.of("MDS")) ))); var result = edcClient().brokerServerApi().catalogPage(query); @@ -106,19 +106,19 @@ void test_available_filter_values_to_filter_by() { createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: Example1 createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "de" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "de" ), "http://my-connector/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "en" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "en" ), "http://my-connector2/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset2", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" + AssetProperty.ASSET_ID, "urn:artifact:my-asset2", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" ), "http://my-connector2/ids/data"); // Dataspace: MDS // get all available filter values @@ -208,7 +208,7 @@ void testAvailableFilters_noFilter() { assertThat(result.getAvailableFilters().getFields()) .extracting(CnfFilterAttribute::getTitle) .containsExactly( - "Data Space", + "Data Space", "Data Category", "Data Subcategory", "Data Model", @@ -248,10 +248,12 @@ void testAvailableFilters_withFilter() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category" + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.DATA_SUBCATEGORY, "my-subcategory" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_SUBCATEGORY, "my-other-subcategory" ), "http://my-connector/ids/data"); @@ -261,12 +263,18 @@ void testAvailableFilters_withFilter() { ))); var result = edcClient().brokerServerApi().catalogPage(query); - var actual = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); - assertThat(actual.getId()).isEqualTo(AssetProperty.DATA_CATEGORY); - assertThat(actual.getTitle()).isEqualTo("Data Category"); - assertThat(actual.getValues()).extracting(CnfFilterItem::getId).containsExactly(""); - assertThat(actual.getValues()).extracting(CnfFilterItem::getTitle).containsExactly(""); + var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); + assertThat(dataCategory.getId()).isEqualTo(AssetProperty.DATA_CATEGORY); + assertThat(dataCategory.getTitle()).isEqualTo("Data Category"); + assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-category", ""); + assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-category", ""); + + var dataSubcategory = getAvailableFilter(result, AssetProperty.DATA_SUBCATEGORY); + assertThat(dataSubcategory.getId()).isEqualTo(AssetProperty.DATA_SUBCATEGORY); + assertThat(dataSubcategory.getTitle()).isEqualTo("Data Subcategory"); + assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-other-subcategory"); + assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-other-subcategory"); }); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java new file mode 100644 index 000000000..2b5e926a8 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +class CollectionUtils2Test { + @Test + void difference() { + assertThat(CollectionUtils2.difference(List.of(1, 2, 3), List.of(2, 3, 4))).containsExactly(1); + } + + @Test + void isNotEmpty_withEmptyList() { + assertThat(CollectionUtils2.isNotEmpty(List.of())).isFalse(); + } + + @Test + void isNotEmpty_withNull() { + assertThat(CollectionUtils2.isNotEmpty(null)).isFalse(); + } + + @Test + void isNotEmpty_withNonEmptyList() { + assertThat(CollectionUtils2.isNotEmpty(List.of(1))).isTrue(); + } + + + @Test + void allElementsWithoutGivenIndex_start() { + assertThat(CollectionUtils2.allElementsExceptForIndex(List.of("A", "B", "C", "D"), 0)).containsExactly("B", "C", "D"); + } + + @Test + void allElementsWithoutGivenIndex_middle() { + assertThat(CollectionUtils2.allElementsExceptForIndex(List.of("A", "B", "C", "D"), 2)).containsExactly("A", "B", "D"); + } + + @Test + void allElementsWithoutGivenIndex_end() { + assertThat(CollectionUtils2.allElementsExceptForIndex(List.of("A", "B", "C", "D"), 3)).containsExactly("A", "B", "C"); + } +} From 519074a024fe6189a5da3dfc15e404daea0cdae8 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 21 Jun 2023 14:13:42 +0200 Subject: [PATCH 061/295] fix: npe from empty arrays returned by db query (#134) --- .../CatalogQueryAvailableFilterFetcher.java | 5 ++++- .../services/api/CatalogApiTest.java | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java index 4e312acc1..564f20a38 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java @@ -20,6 +20,7 @@ import org.jooq.Field; import org.jooq.JSON; import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; import java.util.ArrayList; import java.util.List; @@ -62,7 +63,9 @@ private Field queryFilterValues( var c = fields.getConnectorTable(); var d = fields.getDataOfferTable(); - return DSL.select(DSL.arrayAggDistinct(currentFilter.valueQuery().getAttributeValueField(fields))) + var value = currentFilter.valueQuery().getAttributeValueField(fields); + + return DSL.select(DSL.coalesce(DSL.arrayAggDistinct(value), DSL.array().cast(SQLDataType.VARCHAR.array()))) .from(d) .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) .where(catalogQueryFilterService.filter(fields, searchQuery, otherFilters)) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 5ef1547b7..e5edb3464 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -160,6 +160,28 @@ void testDataOfferDetails() { }); } + /** + * Tests against an issue where empty available filter values resulted in NULLs + */ + @Test + void testEmptyConnector() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + createConnector(dsl, today, "http://my-connector/ids/data"); + + // act + var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + + // assert + assertThat(result.getDataOffers()).isEmpty(); + assertThat(result.getAvailableFilters().getFields()).isNotEmpty(); + assertThat(result.getAvailableSortings()).isNotEmpty(); + + // the most important thing is that the above code ran through as it crashed before + }); + } + @Test void testAvailableFilters_noFilter() { TEST_DATABASE.testTransaction(dsl -> { From 3c125a342eef5a9b3444ccdd96ea2a3e460d4fba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 07:28:43 +0200 Subject: [PATCH 062/295] chore(deps): bump org.flywaydb.flyway from 9.19.4 to 9.20.0 (#135) Bumps org.flywaydb.flyway from 9.19.4 to 9.20.0. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 566d66222..4507aed14 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -27,7 +27,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.19.4" + id("org.flywaydb.flyway") version "9.20.0" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From b10ca7ca7be4afa34044c429c309c739b721d9e9 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 22 Jun 2023 09:48:26 +0200 Subject: [PATCH 063/295] fix: add vault-filesystem dependendcy --- connector/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index 2fce034a4..0054f7d65 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { // Optional: Connector-To-Connector IAM if (project.hasProperty("oauth2")) { + implementation("${edcGroup}:vault-filesystem:${edcVersion}") implementation("${edcGroup}:oauth2-core:${edcVersion}") } else { implementation("${edcGroup}:iam-mock:${edcVersion}") From 15abb9785628cfba1b10756d626794ad1826ffb9 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 22 Jun 2023 10:22:00 +0200 Subject: [PATCH 064/295] fix: tasks not being run in parallel (#136) --- .../BrokerServerExtensionContextBuilder.java | 2 +- .../services/config/BrokerServerSettings.java | 2 ++ .../config/BrokerServerSettingsFactory.java | 2 ++ .../services/queue/ConnectorQueue.java | 2 +- .../services/queue/ThreadPool.java | 36 ++++++++++++++----- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index ebedefdef..7ab2ac395 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -147,7 +147,7 @@ public static BrokerServerExtensionContext buildContext( var policyDtoBuilder = new PolicyDtoBuilder(objectMapper); var assetPropertyParser = new AssetPropertyParser(objectMapper); var paginationMetadataUtils = new PaginationMetadataUtils(); - var threadPool = new ThreadPool(config); + var threadPool = new ThreadPool(brokerServerSettings, monitor); var connectorQueue = new ConnectorQueue(connectorUpdater, threadPool); var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); var connectorCreator = new ConnectorCreator(connectorQueries); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java index d5373ea71..d0fdaa67b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java @@ -27,4 +27,6 @@ public class BrokerServerSettings { int catalogPagePageSize; DataSpaceConfig dataSpaceConfig; + + int numThreads; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java index eab79eb00..c73e0d596 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -32,11 +32,13 @@ public BrokerServerSettings buildBrokerServerSettings(Config config) { var hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); var dataSpaceConfig = buildDataSpaceConfig(config); + var numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); return BrokerServerSettings.builder() .hideOfflineDataOffersAfter(hideOfflineDataOffersAfter) .catalogPagePageSize(catalogPagePageSize) .dataSpaceConfig(dataSpaceConfig) + .numThreads(numThreads) .build(); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java index f8ba03b1c..a1d790494 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java @@ -37,7 +37,7 @@ public void addAll(Collection endpoints, int priority) { endpoints.removeIf(queuedConnectorEndpoints::contains); for (String endpoint : endpoints) { - threadPool.execute(priority, () -> connectorUpdater.updateConnector(endpoint), endpoint); + threadPool.enqueueConnectorRefreshTask(priority, () -> connectorUpdater.updateConnector(endpoint), endpoint); } } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java index 541c21e34..da80fd171 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java @@ -14,8 +14,8 @@ package de.sovity.edc.ext.brokerserver.services.queue; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import org.eclipse.edc.spi.system.configuration.Config; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import org.eclipse.edc.spi.monitor.Monitor; import java.util.ArrayList; import java.util.Objects; @@ -26,19 +26,28 @@ import java.util.stream.Collectors; public class ThreadPool { + private final boolean enabled; private final PriorityBlockingQueue queue; + private final ThreadPoolExecutor threadPoolExecutor; - public ThreadPool(Config config) { - this.queue = new PriorityBlockingQueue<>(); - int numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); - if (numThreads > 0) { - var threadPoolExecutor = new ThreadPoolExecutor(1, numThreads, 60, TimeUnit.SECONDS, queue); + public ThreadPool(BrokerServerSettings brokerServerSettings, Monitor monitor) { + queue = new PriorityBlockingQueue<>(); + + int numThreads = brokerServerSettings.getNumThreads(); + enabled = numThreads > 0; + + if (enabled) { + monitor.info("Initializing ThreadPoolExecutor with %d threads.".formatted(numThreads)); + threadPoolExecutor = new ThreadPoolExecutor(1, numThreads, 60, TimeUnit.SECONDS, queue); threadPoolExecutor.prestartAllCoreThreads(); + } else { + monitor.info("Skipped ThreadPoolExecutor initialization."); + threadPoolExecutor = null; } } - public void execute(int priority, Runnable runnable, String endpoint) { - queue.add(new ThreadPoolTask(priority, runnable, endpoint)); + public void enqueueConnectorRefreshTask(int priority, Runnable runnable, String endpoint) { + enqueueTask(new ThreadPoolTask(priority, runnable, endpoint)); } public Set getQueuedConnectorEndpoints() { @@ -50,4 +59,13 @@ public Set getQueuedConnectorEndpoints() { .filter(Objects::nonNull) .collect(Collectors.toSet()); } + + private void enqueueTask(ThreadPoolTask task) { + if (enabled) { + threadPoolExecutor.execute(task); + } else { + // Only relevant for test environment, where execution is disabled + queue.add(task); + } + } } From a54a4e51af9c87171f71a21e118b08eb84820425 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:50:14 +0200 Subject: [PATCH 065/295] refactor: remove management-api (#145) --- connector/build.gradle.kts | 1 - extensions/broker-server/build.gradle.kts | 1 - 2 files changed, 2 deletions(-) diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index 0054f7d65..0ae99e7b7 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -13,7 +13,6 @@ val sovityEdcExtensionsVersion: String by project dependencies { // Control-Plane implementation("${edcGroup}:control-plane-core:${edcVersion}") - implementation("${edcGroup}:management-api:${edcVersion}") implementation("${edcGroup}:api-observability:${edcVersion}") implementation("${edcGroup}:configuration-filesystem:${edcVersion}") implementation("${edcGroup}:control-plane-aggregate-services:${edcVersion}") diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 079fc4b7f..2d1dab7fc 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -40,7 +40,6 @@ dependencies { testImplementation("${edcGroup}:http:${edcVersion}") testImplementation("${edcGroup}:iam-mock:${edcVersion}") testImplementation("${edcGroup}:ids:${edcVersion}") - testImplementation("${edcGroup}:management-api:${edcVersion}") testImplementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") testImplementation("${edcGroup}:configuration-filesystem:${edcVersion}") testImplementation("${sovityEdcGroup}:client:${sovityEdcExtensionsVersion}") From cfefbc8afc1b6f3d7301771cd28d102f2ffc0aad Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 22 Jun 2023 15:29:25 +0200 Subject: [PATCH 066/295] fix: thread pool not executing in parallel (#147) --- .../services/queue/ThreadPool.java | 2 +- .../services/queue/ThreadPoolTest.java | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java index da80fd171..8de97bdf2 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java @@ -38,7 +38,7 @@ public ThreadPool(BrokerServerSettings brokerServerSettings, Monitor monitor) { if (enabled) { monitor.info("Initializing ThreadPoolExecutor with %d threads.".formatted(numThreads)); - threadPoolExecutor = new ThreadPoolExecutor(1, numThreads, 60, TimeUnit.SECONDS, queue); + threadPoolExecutor = new ThreadPoolExecutor(numThreads, numThreads, 60, TimeUnit.SECONDS, queue); threadPoolExecutor.prestartAllCoreThreads(); } else { monitor.info("Skipped ThreadPoolExecutor initialization."); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java new file mode 100644 index 000000000..c8d5d6105 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.queue; + +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import lombok.SneakyThrows; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ThreadPoolTest { + + @Test + void testParallelExecution() { + var monitor = mock(Monitor.class); + var brokerServerSettings = mock(BrokerServerSettings.class); + when(brokerServerSettings.getNumThreads()).thenReturn(2); + var threadPool = new ThreadPool(brokerServerSettings, monitor); + + var result = new ArrayList(); + threadPool.enqueueConnectorRefreshTask(0, delay(100, () -> result.add("1")), "1"); + threadPool.enqueueConnectorRefreshTask(0, delay(50, () -> result.add("2")), "2"); + + assertThat(result).containsExactly("2", "1"); + } + + Runnable delay(int delayInMs, Runnable onDone) { + return () -> { + safeSleep(delayInMs); + onDone.run(); + }; + } + + @SneakyThrows + void safeSleep(int delayInMs) { + Thread.sleep(delayInMs); + } +} From 1793a984c400e5c68b4e16fa68bb1552e2574f7b Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 22 Jun 2023 15:44:06 +0200 Subject: [PATCH 067/295] fix: try fix pipeline (#148) --- .../edc/ext/brokerserver/services/queue/ThreadPoolTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java index c8d5d6105..b581cec1b 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java @@ -33,10 +33,10 @@ void testParallelExecution() { var brokerServerSettings = mock(BrokerServerSettings.class); when(brokerServerSettings.getNumThreads()).thenReturn(2); var threadPool = new ThreadPool(brokerServerSettings, monitor); - var result = new ArrayList(); threadPool.enqueueConnectorRefreshTask(0, delay(100, () -> result.add("1")), "1"); threadPool.enqueueConnectorRefreshTask(0, delay(50, () -> result.add("2")), "2"); + safeSleep(200); assertThat(result).containsExactly("2", "1"); } From d0f51a76c66657bab4f5311883246b574cd40ee3 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 22 Jun 2023 16:59:11 +0200 Subject: [PATCH 068/295] feat: better logged messages (#149) --- .../services/logging/BrokerEventLogger.java | 72 +++++++------------ .../logging/ConnectorChangeTracker.java | 14 +--- .../ConnectorUpdateFailureWriter.java | 16 +---- .../ConnectorUpdateSuccessWriter.java | 4 +- .../offers/DataOfferLimitsEnforcer.java | 4 +- .../logging/BrokerEventLoggerTest.java | 8 +-- .../offers/DataOfferLimitsEnforcerTest.java | 4 +- 7 files changed, 39 insertions(+), 83 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index 83e32e7a1..be478212f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -17,7 +17,6 @@ import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.BrokerEventLogRecord; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -30,44 +29,34 @@ @RequiredArgsConstructor public class BrokerEventLogger { - public void logConnectorUpdateSuccess(DSLContext dsl, String connectorEndpoint, ConnectorChangeTracker changes) { - var logEntry = logEntry( - dsl, - BrokerEventType.CONNECTOR_UPDATED, - connectorEndpoint, - changes.toString() - ); + public void logConnectorUpdated(DSLContext dsl, String connectorEndpoint, ConnectorChangeTracker changes) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setUserMessage(changes.toString()); logEntry.insert(); } - public void logConnectorUpdateFailure(DSLContext dsl, String connectorEndpoint, BrokerEventErrorMessage errorMessage) { - var logEntry = logEntry( - dsl, - BrokerEventType.CONNECTOR_UPDATED, - connectorEndpoint, - errorMessage.message() - ); + public void logConnectorOffline(DSLContext dsl, String connectorEndpoint, BrokerEventErrorMessage errorMessage) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE); logEntry.setEventStatus(BrokerEventStatus.ERROR); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setUserMessage("Connector is offline."); logEntry.setErrorStack(errorMessage.stackTraceOrNull()); logEntry.insert(); } - public void logConnectorUpdateStatusChange(DSLContext dsl, String connectorEndpoint, ConnectorOnlineStatus status) { - var logEntry = switch (status) { - case ONLINE -> logEntry( - dsl, - BrokerEventType.CONNECTOR_STATUS_CHANGE_ONLINE, - connectorEndpoint, - "Connector is online: " + connectorEndpoint - ); - case OFFLINE -> logEntry( - dsl, - BrokerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE, - connectorEndpoint, - "Connector is offline: " + connectorEndpoint - ); - default -> throw new IllegalArgumentException("Unknown status: " + status + " for connector: " + connectorEndpoint); - }; + public void logConnectorOnline(DSLContext dsl, String connectorEndpoint) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_ONLINE); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setUserMessage("Connector is online."); logEntry.insert(); } @@ -76,17 +65,17 @@ public void logConnectorUpdateDataOfferLimitExceeded(Integer maxDataOffersPerCon logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector has exceeded the maximum number of data offers: " + maxDataOffersPerConnector); + logEntry.setUserMessage("Connector has more than %d data offers. Exceeding data offers will be ignored.".formatted(maxDataOffersPerConnector)); logEntry.setCreatedAt(OffsetDateTime.now()); logEntry.insert(); } - public void logConnectorUpdateDataOfferLimitOk(Integer maxDataOffersPerConnector, String endpoint) { + public void logConnectorUpdateDataOfferLimitOk(String endpoint) { var logEntry = new BrokerEventLogRecord(); logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_OK); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector is not exceeding maximum number of data offers limits anymore: " + maxDataOffersPerConnector); + logEntry.setUserMessage("Connector is not exceeding the maximum number of data offers limit anymore."); logEntry.setCreatedAt(OffsetDateTime.now()); logEntry.insert(); } @@ -96,28 +85,19 @@ public void logConnectorUpdateContractOfferLimitExceeded(Integer maxContractOffe logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector has exceeded maximum number of contract offers per data offer limit: " + maxContractOffersPerConnector); + logEntry.setUserMessage("Some data offers have more than %d contract offers. Exceeding contract offers will be ignored.: ".formatted(maxContractOffersPerConnector)); logEntry.setCreatedAt(OffsetDateTime.now()); logEntry.insert(); } - public void logConnectorUpdateContractOfferLimitOk(Integer maxContractOffersPerConnector, String endpoint) { + public void logConnectorUpdateContractOfferLimitOk(String endpoint) { var logEntry = new BrokerEventLogRecord(); logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_OK); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector is not exceeding maximum number of contract offers per data offer limits anymore: " + maxContractOffersPerConnector); + logEntry.setUserMessage("Connector is not exceeding the maximum number of contract offers per data offer limit anymore."); logEntry.setCreatedAt(OffsetDateTime.now()); logEntry.insert(); } - private BrokerEventLogRecord logEntry(DSLContext dsl, BrokerEventType eventType, String connectorEndpoint, String userMessage) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setEvent(eventType); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage(userMessage); - return logEntry; - } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java index b4ad140ca..b34688f7c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java @@ -20,15 +20,11 @@ import java.util.ArrayList; import java.util.List; -import static java.util.stream.Collectors.joining; - /** * Utility for collecting the information required to build log messages about what was updated. */ @Getter public class ConnectorChangeTracker { - private final List selfDescriptionChanges = new ArrayList<>(); - @Setter private int numOffersAdded = 0; @@ -38,13 +34,8 @@ public class ConnectorChangeTracker { @Setter private int numOffersUpdated = 0; - - public void addSelfDescriptionChange(String name) { - selfDescriptionChanges.add(name); - } - public boolean isEmpty() { - return selfDescriptionChanges.isEmpty() && numOffersAdded == 0 && numOffersDeleted == 0 && numOffersUpdated == 0; + return numOffersAdded == 0 && numOffersDeleted == 0 && numOffersUpdated == 0; } @Override @@ -54,9 +45,6 @@ public String toString() { } var msg = "Connector Updated."; - if (!selfDescriptionChanges.isEmpty()) { - msg += " Self-description changed: %s.".formatted(selfDescriptionChanges.stream().sorted().collect(joining())); - } if (numOffersAdded > 0 || numOffersDeleted > 0 || numOffersUpdated > 0) { List offersMsgs = new ArrayList<>(); if (numOffersAdded > 0) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java index 5e1f2856c..87dd61389 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java @@ -19,7 +19,6 @@ import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventErrorMessage; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; import org.jooq.DSLContext; @@ -33,10 +32,9 @@ public class ConnectorUpdateFailureWriter { public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Throwable e) { // Log Status Change and set status to offline if necessary if (connector.getOnlineStatus() == ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { - brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.OFFLINE); - brokerEventLogger.logConnectorUpdateFailure(dsl, connector.getEndpoint(), getFailureMessage(e)); - connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); monitor.info("Connector is offline: " + connector.getEndpoint(), e); + brokerEventLogger.logConnectorOffline(dsl, connector.getEndpoint(), getFailureMessage(e)); + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); } connector.setLastRefreshAttemptAt(OffsetDateTime.now()); @@ -44,14 +42,6 @@ public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Th } public BrokerEventErrorMessage getFailureMessage(Throwable e) { - if (isUnexpectedException(e)) { - return BrokerEventErrorMessage.ofStackTrace("Unexpected exception during connector update.", e); - } - - return BrokerEventErrorMessage.ofMessage("Failed updating connector."); - } - - private boolean isUnexpectedException(Throwable e) { - return !(e instanceof EdcException); + return BrokerEventErrorMessage.ofStackTrace("Unexpected exception during connector update.", e); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 9af4f7a20..40aafe6b1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -46,7 +46,7 @@ public void handleConnectorOnline( // Log Status Change and set status to online if necessary if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE || connector.getLastRefreshAttemptAt() == null) { - brokerEventLogger.logConnectorUpdateStatusChange(dsl, connector.getEndpoint(), ConnectorOnlineStatus.ONLINE); + brokerEventLogger.logConnectorOnline(dsl, connector.getEndpoint()); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); } @@ -58,7 +58,7 @@ public void handleConnectorOnline( // Log Event if changes are present if (!changes.isEmpty()) { - brokerEventLogger.logConnectorUpdateSuccess(dsl, connector.getEndpoint(), changes); + brokerEventLogger.logConnectorUpdated(dsl, connector.getEndpoint(), changes); } // Update data offers diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java index 3ee5a1e51..7a03e9751 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java @@ -74,7 +74,7 @@ public void logEnforcedLimitsIfChanged(ConnectorRecord connector, DataOfferLimit brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.EXCEEDED); } else if (!enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.EXCEEDED) { - brokerEventLogger.logConnectorUpdateDataOfferLimitOk(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + brokerEventLogger.logConnectorUpdateDataOfferLimitOk(connector.getEndpoint()); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); } @@ -83,7 +83,7 @@ public void logEnforcedLimitsIfChanged(ConnectorRecord connector, DataOfferLimit brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.EXCEEDED); } else if (!enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.EXCEEDED) { - brokerEventLogger.logConnectorUpdateContractOfferLimitOk(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + brokerEventLogger.logConnectorUpdateContractOfferLimitOk(connector.getEndpoint()); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java index ddaacf050..1aca8a484 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java @@ -17,7 +17,6 @@ import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -37,10 +36,9 @@ void testDataOfferWriter_allSortsOfUpdates() { var brokerEventLogger = new BrokerEventLogger(); // Test that insertions insert required fields and don't cause DB errors - brokerEventLogger.logConnectorUpdateSuccess(dsl, "https://example.com/ids/data", new ConnectorChangeTracker()); - brokerEventLogger.logConnectorUpdateFailure(dsl, "https://example.com/ids/data", new BrokerEventErrorMessage("Message", "Stacktrace")); - brokerEventLogger.logConnectorUpdateStatusChange(dsl, "https://example.com/ids/data", ConnectorOnlineStatus.ONLINE); - brokerEventLogger.logConnectorUpdateStatusChange(dsl, "https://example.com/ids/data", ConnectorOnlineStatus.OFFLINE); + brokerEventLogger.logConnectorUpdated(dsl, "https://example.com/ids/data", new ConnectorChangeTracker()); + brokerEventLogger.logConnectorOnline(dsl, "https://example.com/ids/data"); + brokerEventLogger.logConnectorOffline(dsl, "https://example.com/ids/data", new BrokerEventErrorMessage("Message", "Stacktrace")); }); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java index 5335f6899..84e055198 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java @@ -165,7 +165,7 @@ void verify_logConnectorUpdateDataOfferLimitOk() { dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateDataOfferLimitOk(2, connector.getEndpoint()); + verify(brokerEventLogger).logConnectorUpdateDataOfferLimitOk(connector.getEndpoint()); } @Test @@ -213,6 +213,6 @@ void verify_logConnectorUpdateContractOfferLimitOk() { dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateContractOfferLimitOk(2, connector.getEndpoint()); + verify(brokerEventLogger).logConnectorUpdateContractOfferLimitOk(connector.getEndpoint()); } } From 00be7f06f7663953b02c05f21971c52674056e46 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 23 Jun 2023 11:36:23 +0200 Subject: [PATCH 069/295] chore: release prep + cleanup (#152) --- .env | 6 +- CHANGELOG.md | 79 +++++++++++++++++-- README.md | 6 ++ connector/.env | 20 +++-- docker-compose.yaml | 3 +- .../brokerserver/BrokerServerExtension.java | 20 ++--- .../BrokerServerExtensionContextBuilder.java | 4 +- .../config/BrokerServerSettingsFactory.java | 56 ++++++------- .../config/EdcConfigPropertyUtils.java | 39 +++++++++ .../offers/DataOfferLimitsEnforcer.java | 2 +- .../services/api/CatalogApiTest.java | 38 +++++---- .../offers/DataOfferLimitsEnforcerTest.java | 14 ++-- 12 files changed, 210 insertions(+), 77 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java diff --git a/.env b/.env index 5e0e82601..e8136c0fe 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:0.1.0 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:3.3.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 54cf27269..b27e28a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,23 +15,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Patch -- Fix: Data Offer Filter available values are no longer limited to the selected value if a value is selected. +## [v0.1.0] Broker MvP Release - 2023-06-23 -## [v0.0.1] Broker PoC Release - 2023-06-02 +### Overview -Initial Broker PoC Release with a minimalistic feature set. +Broker MvP using Core EDC MS8. + +### Detailed Changes + +#### Minor + +- Implemented Catalog Page Filters: + - Data Space Filter + - Data Category + - Data Subcategory + - Data Model + - Transport Mode + - Geo Reference Method +- Implemented Catalog Page Sorting: + - Most Recent + - By Title + - By Connector +- Implemented Catalog Page Pagination. + +#### Patch + +- Fix: Data Offer Filter available values are no longer limited to the selected value if a value is selected. +- Fix: Missing file system vault prevented data space login. +- Fix: Parallel crawling was not actually parallel ### Deployment Migration Notes -Please view the [Deployment Section in the README.md](README.md#deployment) for initial deployment instructions. +1. There are new **required** configuration properties: + ```yaml + # List of Data Space Names for special Connectors (default: '') + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/ids/data,OtherDataspace=https://some-other-connector/ids/data" + ``` +2. There are new **optional** configuration properties available for overriding: + ```yaml + # Parallelization for Crawling (default: 3) + EDC_BROKER_SERVER_NUM_THREADS: 16 + + # Default Data Space Name (default: MDS) + EDC_BROKER_SERVER_DEFAULT_DATASPACE: MDS + + # Maximum number of Data Offers per Connector (default: 50) + EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR: 50 + + # Maximum number of Contract Offers per Data Offer (default: 10) + EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER: 10 + + # Pagination Configuration: Catalog Page Size (default: 20) + EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE: 20 + ``` +3. An issue prevented the keystore file from being read, preventing a successful data space log in. +4. Added a reference to [connector/.env](connector/.env) as source for other possible broker server configuration + options, that have defaults, but might have use cases for overriding. #### Compatible Versions -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.0.1` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6` +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.1.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity8` - Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) -### Major +## [v0.0.1] Broker PoC Release - 2023-06-02 + +### Overview + +Initial Broker PoC Release with a minimalistic feature set. + +### Detailed Changes + +#### Major - Implemented a Broker PoC with EDC MS8: - Periodic Crawling of Connectors @@ -39,3 +94,13 @@ Please view the [Deployment Section in the README.md](README.md#deployment) for - Query Connectors via UI - Persistence of Connector Status Updates - Persistence of Crawling Execution Times + +### Deployment Migration Notes + +Please view the [Deployment Section in the README.md](README.md#deployment) for initial deployment instructions. + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.0.1` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6` +- Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) \ No newline at end of file diff --git a/README.md b/README.md index b4ce3384f..afebb19c4 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ MY_EDC_JDBC_PASSWORD: edc # Required: List of EDCs to fetch EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/ids/data,https://connector-b/ids/data" +# List of Data Space Names for special Connectors (default: '') +EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/ids/data,OtherDataspace=https://some-other-connector/ids/data" + # Required: DAPS credentials EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' @@ -124,6 +127,9 @@ EDC_KEYSTORE_PASSWORD: '_your keystore password_' EDC_API_AUTH_KEY: "ApiKeyDefaultValue" ``` +All pre-configured config values for either the broker server or the underlying EDC can be found +in [connector/.env](connector/.env). + #### UI Configuration ```yaml diff --git a/connector/.env b/connector/.env index dada64602..e2b7d45dc 100644 --- a/connector/.env +++ b/connector/.env @@ -19,22 +19,30 @@ MY_EDC_JDBC_PASSWORD=missing-postgresql-password # List of Connectors to be added on startup EDC_BROKER_SERVER_KNOWN_CONNECTORS= +# Default Data Space Name +EDC_BROKER_SERVER_DEFAULT_DATASPACE=MDS + +# List of Data Space Names for special Connectors +# e.g. Mobilithek=https://my-connector1/ids/data,SomeOtherDataspace=https://my-connector2/ids/data +EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS= + # Frequency of refreshing connectors EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * # Duration a data offer is shown for offline connectors EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D -# Parallelization for crawling +# Parallelization for Crawling EDC_BROKER_SERVER_NUM_THREADS=3 + +# Maximum number of Data Offers per Connector EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR=50 -EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_CONNECTOR=10 -# Pagination Defaults -EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 +# Maximum number of Contract Offers per Data Offer +EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER=10 -EDC_BROKER_SERVER_DEFAULT_DATASPACE=MDS -EDC_BROKER_SERVER_KNOWN_DATASPACES_ENDPOINTS=Example1=http://connector-endpoint1.org;Example2=http://connector-endpoint2.org +# Pagination Configuration: Catalog Page Size +EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 # =========================================================== # Other EDC Config diff --git a/docker-compose.yaml b/docker-compose.yaml index 702a9a6dc..7fc1eb498 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,6 +20,7 @@ services: MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/ids/data" # Local Dev / Docker-Compose Config MY_EDC_PROTOCOL: "http://" # We don't have TLS in the docker container @@ -28,7 +29,7 @@ services: EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" # Update connectors every 20s in dev - EDC_BROKER_SERVER_NUM_THREADS: "1" # No parallelism in dev + EDC_BROKER_SERVER_NUM_THREADS: "16" EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" # Hide offline data offers after 1 minute in dev EDC_API_AUTH_KEY: "ApiKeyDefaultValue" # Management API Key (Access to UI should be secured by other means, as this key is sent to the UI) ports: diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index abc2fa2cf..9102624ae 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -23,36 +23,38 @@ import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.web.spi.WebService; +import static de.sovity.edc.ext.brokerserver.services.config.EdcConfigPropertyUtils.toEdcProp; + public class BrokerServerExtension implements ServiceExtension { public static final String EXTENSION_NAME = "BrokerServerExtension"; @Setting - public static final String KNOWN_CONNECTORS = "edc.broker.server.known.connectors"; + public static final String KNOWN_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_CONNECTORS"); @Setting - public static final String CRON_CONNECTOR_REFRESH = "edc.broker.server.cron.connector.refresh"; + public static final String CRON_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH"); @Setting - public static final String NUM_THREADS = "edc.broker.server.num.threads"; + public static final String NUM_THREADS = toEdcProp("EDC_BROKER_SERVER_NUM_THREADS"); @Setting - public static final String HIDE_OFFLINE_DATA_OFFERS_AFTER = "edc.broker.server.hide.offline.data.offers.after"; + public static final String HIDE_OFFLINE_DATA_OFFERS_AFTER = toEdcProp("EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER"); @Setting - public static final String MAX_DATA_OFFERS_PER_CONNECTOR = "edc.broker.server.max.data.offers.per.connector"; + public static final String MAX_DATA_OFFERS_PER_CONNECTOR = toEdcProp("EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR"); @Setting - public static final String MAX_CONTRACT_OFFERS_PER_CONNECTOR = "edc.broker.server.max.contract.offers.per.connector"; + public static final String MAX_CONTRACT_OFFERS_PER_DATA_OFFER = toEdcProp("EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER"); @Setting - public static final String CATALOG_PAGE_PAGE_SIZE = "edc.broker.server.catalog.page.page.size"; + public static final String CATALOG_PAGE_PAGE_SIZE = toEdcProp("EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE"); @Setting - public static final String DEFAULT_CONNECTOR_DATASPACE = "edc.broker.server.default.dataspace"; + public static final String DEFAULT_CONNECTOR_DATASPACE = toEdcProp("EDC_BROKER_SERVER_DEFAULT_DATASPACE"); @Setting - public static final String KNOWN_DATASPACES_ENDPOINTS = "edc.broker.server.known.dataspaces.endpoints"; + public static final String KNOWN_DATASPACE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS"); @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 7ab2ac395..97ebedc05 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -84,8 +84,8 @@ public static BrokerServerExtensionContext buildContext( TypeManager typeManager, CatalogService catalogService ) { - var brokerServerSettingsFactory = new BrokerServerSettingsFactory(); - var brokerServerSettings = brokerServerSettingsFactory.buildBrokerServerSettings(config); + var brokerServerSettingsFactory = new BrokerServerSettingsFactory(config, monitor); + var brokerServerSettings = brokerServerSettingsFactory.buildBrokerServerSettings(); // Dao var dataOfferQueries = new DataOfferQueries(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java index c73e0d596..d0e24c9ef 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -16,19 +16,21 @@ import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import lombok.NonNull; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; import java.time.Duration; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +@RequiredArgsConstructor public class BrokerServerSettingsFactory { - private Config config; - - public BrokerServerSettings buildBrokerServerSettings(Config config) { - this.config = config; + private final Config config; + private final Monitor monitor; + public BrokerServerSettings buildBrokerServerSettings() { var hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); var dataSpaceConfig = buildDataSpaceConfig(config); @@ -43,31 +45,31 @@ public BrokerServerSettings buildBrokerServerSettings(Config config) { } private DataSpaceConfig buildDataSpaceConfig(Config config) { - return new DataSpaceConfig(getKnownDataSpaceEndpoints(config), getDefaultDataSpace(config)); + var dataSpaceConfig = new DataSpaceConfig(getKnownDataSpaceEndpoints(config), getDefaultDataSpace(config)); + monitor.info("Default Dataspace Name: %s".formatted(dataSpaceConfig.defaultDataSpace())); + dataSpaceConfig.dataSpaceConnectors().forEach(dataSpaceConnector -> monitor.info("Using Dataspace Name %s for %s." + .formatted(dataSpaceConnector.dataSpaceName(), dataSpaceConnector.endpoint()))); + if (dataSpaceConfig.dataSpaceConnectors().isEmpty()) { + monitor.info("No additional data space names configured."); + } + return dataSpaceConfig; } private List getKnownDataSpaceEndpoints(Config config) { - // Example: "Example1=http://connector-endpoint1.org;Example2=http://connector-endpoint2.org" - var defaultDataSpaces = new ArrayList(); - var dataSpacesConfig = config.getString(BrokerServerExtension.KNOWN_DATASPACES_ENDPOINTS, ""); - - var allDataSpaces = dataSpacesConfig.split(";"); - for (var dataSpace : allDataSpaces) { - var dataSpaceParts = dataSpace.split("="); - if (dataSpaceParts.length != 2) { - continue; - } - - var dataSpaceName = dataSpaceParts[0].trim(); - var dataSpaceEndpoint = dataSpaceParts[1].trim(); - if (StringUtils.isBlank(dataSpaceName) || StringUtils.isBlank(dataSpaceEndpoint)) { - continue; - } - - defaultDataSpaces.add(new DataSpaceConnector(dataSpaceEndpoint, dataSpaceName)); - } - - return defaultDataSpaces; + // Example: "Example1=http://connector-endpoint1.org,Example2=http://connector-endpoint2.org" + var dataSpacesConfig = config.getString(BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, ""); + + return Arrays.stream(dataSpacesConfig.split(",")) + .map(String::trim) + .map(it -> it.split("=")) + .filter(it -> it.length == 2) + .map(it -> { + var dataSpaceName = it[0].trim(); + var dataSpaceEndpoint = it[1].trim(); + return new DataSpaceConnector(dataSpaceEndpoint, dataSpaceName); + }) + .filter(it -> StringUtils.isNotBlank(it.endpoint()) && StringUtils.isNotBlank(it.endpoint())) + .toList(); } private String getDefaultDataSpace(Config config) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java new file mode 100644 index 000000000..4ce3fd64b --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.config; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Arrays; + +import static java.util.stream.Collectors.joining; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EdcConfigPropertyUtils { + /** + * For better refactoring it is better if the string constant is + * found in the code as it is used and documented. + * + * @param envVarName e.g. "BROKER_SERVER_SOME_CONFIG_SETTING" + * @return e.g. "broker.server.some.config.setting" + */ + public static String toEdcProp(String envVarName) { + return Arrays.stream(envVarName.split("_")) + .map(String::toLowerCase) + .collect(joining(".")); + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java index 7a03e9751..b9ec5efb4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java @@ -40,7 +40,7 @@ public record DataOfferLimitsEnforced( public DataOfferLimitsEnforced enforceLimits(Collection dataOffers) { // Get limits from config var maxDataOffers = config.getInteger(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, -1); - var maxContractOffers = config.getInteger(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR, -1); + var maxContractOffers = config.getInteger(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER, -1); var offerList = dataOffers.stream().toList(); // No limits set diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index e5edb3464..53c66c807 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -63,7 +63,7 @@ void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACES_ENDPOINTS, "Example1=http://my-connector/ids/data" + BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" ))); } @@ -73,20 +73,20 @@ void testDataSpace_two_dataspaces_filter_for_one() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: Example1 - createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: MDS + createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: MDS + createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset", AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector/ids/data"); // Dataspace: Example1 + ), "http://my-connector/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset", AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector2/ids/data"); // Dataspace: MDS + ), "http://my-connector2/ids/data"); // Dataspace: Example1 var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataSpace", List.of("MDS")) + new CnfFilterValueAttribute("dataSpace", List.of("Example1")) ))); var result = edcClient().brokerServerApi().catalogPage(query); @@ -103,30 +103,40 @@ void test_available_filter_values_to_filter_by() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: Example1 - createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: MDS + createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: MDS + createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 + createConnector(dsl, today, "http://my-connector3/ids/data"); // Dataspace: Example2 createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset", AssetProperty.ASSET_NAME, "my-asset", AssetProperty.LANGUAGE, "de" - ), "http://my-connector/ids/data"); // Dataspace: Example1 + ), "http://my-connector/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset", AssetProperty.ASSET_NAME, "my-asset", AssetProperty.LANGUAGE, "en" - ), "http://my-connector2/ids/data"); // Dataspace: MDS + ), "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset2", AssetProperty.ASSET_NAME, "my-asset", AssetProperty.LANGUAGE, "fr" - ), "http://my-connector2/ids/data"); // Dataspace: MDS + ), "http://my-connector2/ids/data"); // Dataspace: Example1 + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset3", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" + ), "http://my-connector3/ids/data"); // Dataspace: Example2 // get all available filter values - var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()).getAvailableFilters(); + var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); // assert that the filter values are correct - var dataSpace = getAvailableFilter(edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()), "dataSpace"); - assertThat(dataSpace.getValues()).containsExactly(new CnfFilterItem("Example1", "Example1"), new CnfFilterItem("MDS", "MDS")); + var dataSpace = getAvailableFilter(result, "dataSpace"); + assertThat(dataSpace.getValues()).containsExactly( + new CnfFilterItem("Example1", "Example1"), + new CnfFilterItem("Example2", "Example2"), + new CnfFilterItem("MDS", "MDS") + ); }); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java index 84e055198..68d96465e 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java @@ -55,7 +55,7 @@ void no_limit_and_two_dataofffers_and_contractoffer_should_not_limit() { int maxDataOffers = -1; int maxContractOffers = -1; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -79,7 +79,7 @@ void limit_zero_and_one_dataoffers_should_result_to_none() { int maxDataOffers = 0; int maxContractOffers = 0; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var dataOffers = List.of(new FetchedDataOffer()); @@ -101,7 +101,7 @@ void limit_one_and_two_dataoffers_should_result_to_one() { int maxDataOffers = 1; int maxContractOffers = 1; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -130,7 +130,7 @@ void verify_logConnectorUpdateDataOfferLimitExceeded() { int maxDataOffers = 1; int maxContractOffers = 1; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -154,7 +154,7 @@ void verify_logConnectorUpdateDataOfferLimitOk() { int maxDataOffers = -1; int maxContractOffers = -1; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -178,7 +178,7 @@ void verify_logConnectorUpdateContractOfferLimitExceeded() { int maxDataOffers = 1; int maxContractOffers = 1; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -202,7 +202,7 @@ void verify_logConnectorUpdateContractOfferLimitOk() { int maxDataOffers = -1; int maxContractOffers = -1; when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_CONNECTOR), any())).thenReturn(maxContractOffers); + when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); From 907c7de39bac6bbab39efeaa3e80955427c0858e Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 23 Jun 2023 15:10:27 +0200 Subject: [PATCH 070/295] chore: set edc-extensions version to 3.3.0 (#153) --- .../ext/brokerserver/BrokerServerResourceImpl.java | 14 -------------- gradle.properties | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 169949527..3fa99ad16 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -20,12 +20,8 @@ import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageResult; import lombok.RequiredArgsConstructor; @@ -47,14 +43,4 @@ public CatalogPageResult catalogPage(CatalogPageQuery query) { public ConnectorPageResult connectorPage(ConnectorPageQuery query) { return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorPage(dsl, query)); } - - @Override - public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery dataOfferDetailPageQuery) { - throw new IllegalStateException("Not yet implemented!"); - } - - @Override - public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery connectorDetailPageQuery) { - throw new IllegalStateException("Not yet implemented!"); - } } diff --git a/gradle.properties b/gradle.properties index cce85f653..3e92b4516 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=3.3.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From f1edad1eff692c4691f0ddfcfacfa42a4464d1e8 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 23 Jun 2023 15:21:42 +0200 Subject: [PATCH 071/295] chore: post release cleanup (#154) --- .env | 6 +++--- .../ext/brokerserver/BrokerServerResourceImpl.java | 14 ++++++++++++++ gradle.properties | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.env b/.env index e8136c0fe..5e0e82601 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:0.1.0 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:3.3.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity8 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 3fa99ad16..169949527 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -20,8 +20,12 @@ import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageResult; import lombok.RequiredArgsConstructor; @@ -43,4 +47,14 @@ public CatalogPageResult catalogPage(CatalogPageQuery query) { public ConnectorPageResult connectorPage(ConnectorPageQuery query) { return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorPage(dsl, query)); } + + @Override + public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery dataOfferDetailPageQuery) { + throw new IllegalStateException("Not yet implemented!"); + } + + @Override + public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery connectorDetailPageQuery) { + throw new IllegalStateException("Not yet implemented!"); + } } diff --git a/gradle.properties b/gradle.properties index 3e92b4516..cce85f653 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions -sovityEdcExtensionsVersion=3.3.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 77ecf40746001703b25edcc843de9b5d9aa85b56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 25 Jun 2023 18:49:46 +0200 Subject: [PATCH 072/295] chore(deps): bump org.jooq:jooq from 3.18.4 to 3.18.5 (#155) Bumps org.jooq:jooq from 3.18.4 to 3.18.5. --- updated-dependencies: - dependency-name: org.jooq:jooq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 4507aed14..6413177d4 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -34,7 +34,7 @@ plugins { } dependencies { - api("org.jooq:jooq:3.18.4") + api("org.jooq:jooq:3.18.5") api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") jooqGenerator("org.postgresql:postgresql:42.6.0") From b4b394e2017b9cbd6ace49d5b458b31e8051f2b2 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:35:16 +0200 Subject: [PATCH 073/295] chore: fix api-wrapper integration (#156) * chore: fix api-wrapper integration * test: refactor assertEqualJson --- extensions/broker-server/build.gradle.kts | 1 + .../BrokerServerExtensionContextBuilder.java | 2 +- .../brokerserver/services/api/PolicyDtoBuilder.java | 6 +----- .../brokerserver/services/api/CatalogApiTest.java | 13 ++++++++++++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 2d1dab7fc..452944211 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") + testImplementation("org.json:json:20230618") implementation("org.quartz-scheduler:quartz:2.3.2") } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 97ebedc05..b810f26ad 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -144,7 +144,7 @@ public static BrokerServerExtensionContext buildContext( monitor, brokerExecutionTimeLogger ); - var policyDtoBuilder = new PolicyDtoBuilder(objectMapper); + var policyDtoBuilder = new PolicyDtoBuilder(); var assetPropertyParser = new AssetPropertyParser(objectMapper); var paginationMetadataUtils = new PaginationMetadataUtils(); var threadPool = new ThreadPool(brokerServerSettings, monitor); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java index 2fbcc1036..baffa368a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -14,20 +14,16 @@ package de.sovity.edc.ext.brokerserver.services.api; -import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import org.eclipse.edc.policy.model.Policy; @RequiredArgsConstructor public class PolicyDtoBuilder { - private final ObjectMapper objectMapper; @SneakyThrows public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { - var policy = objectMapper.readValue(policyJson, Policy.class); - return new PolicyDto(policy); + return new PolicyDto(policyJson, null); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 53c66c807..738003dde 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -36,6 +36,7 @@ import org.eclipse.edc.policy.model.Policy; import org.jooq.DSLContext; import org.jooq.JSONB; +import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -166,10 +167,20 @@ void testDataOfferDetails() { AssetProperty.ASSET_NAME, "my-asset" )); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); - assertThat(toJson(dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy())).isEqualTo(toJson(dummyPolicy())); + + // Key order of Json-String might differ, so we compare the JSON-Objects for similarity + var actual = dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy(); + var expected = toJson(dummyPolicy()); + assertEqualJson(expected, actual); }); } + private void assertEqualJson(String expected, String actual) { + var expectedJson = new JSONObject(expected); + var actualJson = new JSONObject(actual); + assertThat(actualJson.similar(expectedJson)).isTrue(); + } + /** * Tests against an issue where empty available filter values resulted in NULLs */ From d8a3d9117ab7afae6d6212ab72520f1804d680e2 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 27 Jun 2023 12:11:47 +0200 Subject: [PATCH 074/295] feat: Data Offer & Connector Detail Page (#157) * feat: dataOfferDetailPage * feat: connectorDetailPage * feat: detail pages * feat: detail pages * feat: detail pages * test: detail pages * refactor: minor refactorings * refactor: minor refactorings * refactor: minor test refactorings * refactor: minor test refactorings * refactor: minor test refactorings * refactor: minor test refactorings * refactor: DataOfferDetailPageQueryService * refactor: DataOfferDetailPageQueryService * refactor: DataOfferDetailPageQueryService * refactor: changed models slightly, improved tests --------- Co-authored-by: Tim Berthold Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server/build.gradle.kts | 2 +- .../BrokerServerExtensionContextBuilder.java | 13 +- .../BrokerServerResourceImpl.java | 10 +- .../CatalogQueryContractOfferFetcher.java | 2 +- .../catalog/CatalogQueryDataOfferFetcher.java | 8 +- .../pages/catalog/models/CatalogPageRs.java | 2 +- .../catalog/models/DataOfferListEntryRs.java | 39 +++++ .../connector/ConnectorPageQueryService.java | 10 ++ .../DataOfferDetailPageQueryService.java | 54 +++++++ .../model}/ContractOfferRs.java | 2 +- .../model/DataOfferDetailRs.java} | 4 +- .../services/api/CatalogApiService.java | 38 +++-- .../services/api/ConnectorApiService.java | 26 ++- .../api/DataOfferDetailApiService.java | 78 +++++++++ .../edc/ext/brokerserver/AssertionUtils.java | 29 ++++ .../services/api/CatalogApiTest.java | 17 +- .../services/api/ConnectorApiTest.java | 110 +++++++++++-- .../services/api/DataOfferDetailApiTest.java | 150 ++++++++++++++++++ 18 files changed, 539 insertions(+), 55 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/{catalog/models => dataoffer/model}/ContractOfferRs.java (91%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/{catalog/models/DataOfferRs.java => dataoffer/model/DataOfferDetailRs.java} (90%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 452944211..ba769dec5 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -48,8 +48,8 @@ dependencies { testImplementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.skyscreamer:jsonassert:1.5.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") - testImplementation("org.json:json:20230618") implementation("org.quartz-scheduler:quartz:2.3.2") } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index b810f26ad..a4cbf2325 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -24,6 +24,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQuerySortingService; import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; @@ -32,6 +33,7 @@ import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterAttributeDefinitionService; @@ -108,6 +110,7 @@ public static BrokerServerExtensionContext buildContext( brokerServerSettings ); var connectorPageQueryService = new ConnectorPageQueryService(); + var dataOfferDetailPageQueryService = new DataOfferDetailPageQueryService(catalogQueryContractOfferFetcher, brokerServerSettings); // Services @@ -189,10 +192,18 @@ public static BrokerServerExtensionContext buildContext( connectorPageQueryService, paginationMetadataUtils ); + + var dataOfferDetailApiService = new DataOfferDetailApiService( + dataOfferDetailPageQueryService, + policyDtoBuilder, + assetPropertyParser + ); + var brokerServerResource = new BrokerServerResourceImpl( dslContextFactory, connectorApiService, - catalogApiService + catalogApiService, + dataOfferDetailApiService ); return new BrokerServerExtensionContext( brokerServerResource, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 169949527..db4604218 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -17,6 +17,7 @@ import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; @@ -37,6 +38,7 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final DslContextFactory dslContextFactory; private final ConnectorApiService connectorApiService; private final CatalogApiService catalogApiService; + private final DataOfferDetailApiService dataOfferDetailApiService; @Override public CatalogPageResult catalogPage(CatalogPageQuery query) { @@ -49,12 +51,12 @@ public ConnectorPageResult connectorPage(ConnectorPageQuery query) { } @Override - public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery dataOfferDetailPageQuery) { - throw new IllegalStateException("Not yet implemented!"); + public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery query) { + return dslContextFactory.transactionResult(dsl -> dataOfferDetailApiService.dataOfferDetailPage(dsl, query)); } @Override - public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery connectorDetailPageQuery) { - throw new IllegalStateException("Not yet implemented!"); + public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query) { + return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorDetailPage(dsl, query)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java index 334b345d3..f4dd1f5a3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import lombok.RequiredArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java index 7768192ee..dc978033e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -15,7 +15,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; @@ -42,9 +42,9 @@ public class CatalogQueryDataOfferFetcher { * @param filters filters (queries + filter clauses) * @param sorting sorting * @param pageQuery pagination - * @return {@link Field} of {@link DataOfferRs}s + * @return {@link Field} of {@link DataOfferListEntryRs}s */ - public Field> queryDataOffers( + public Field> queryDataOffers( CatalogQueryFields fields, String searchQuery, List filters, @@ -70,7 +70,7 @@ public Field> queryDataOffers( .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)) .limit(pageQuery.offset(), pageQuery.limit()); - return MultisetUtils.multiset(query, DataOfferRs.class); + return MultisetUtils.multiset(query, DataOfferListEntryRs.class); } /** diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java index 8a7e7f9c9..88a199df1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java @@ -26,6 +26,6 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class CatalogPageRs { String availableFilterValues; - List dataOffers; + List dataOffers; int numTotalDataOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java new file mode 100644 index 000000000..0abc3749a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; + +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; +import java.util.List; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DataOfferListEntryRs { + String assetId; + String assetPropertiesJson; + OffsetDateTime createdAt; + OffsetDateTime updatedAt; + List contractOffers; + String connectorEndpoint; + ConnectorOnlineStatus connectorOnlineStatus; + OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java index 4d842049f..7c766311e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java @@ -38,6 +38,16 @@ public List queryConnectorPage(DSLContext dsl, String searchQuery, .fetchInto(ConnectorRs.class); } + public ConnectorRs queryConnectorDetailPage(DSLContext dsl, String connectorEndpoint) { + var c = Tables.CONNECTOR; + var filterBySearchQuery = SearchUtils.simpleSearch(connectorEndpoint, List.of(c.ENDPOINT, c.CONNECTOR_ID)); + + return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) + .from(c) + .where(filterBySearchQuery) + .fetchOneInto(ConnectorRs.class); + } + @NotNull private List> sortConnectorPage(Connector c, ConnectorPageSortingType sorting) { var alphabetically = c.ENDPOINT.asc(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java new file mode 100644 index 000000000..e47e52829 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryContractOfferFetcher; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.DataOfferDetailRs; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class DataOfferDetailPageQueryService { + private final CatalogQueryContractOfferFetcher catalogQueryContractOfferFetcher; + private final BrokerServerSettings brokerServerSettings; + + public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetId, String endpoint) { + // We are re-using the catalog page query stuff as long as we can get away with it + var fields = new CatalogQueryFields( + Tables.CONNECTOR, + Tables.DATA_OFFER, + brokerServerSettings.getDataSpaceConfig() + ); + + var d = fields.getDataOfferTable(); + var c = fields.getConnectorTable(); + + return dsl.select( + d.ASSET_ID, + d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), + d.CREATED_AT, + d.UPDATED_AT, + catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), + fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt"), + c.ENDPOINT.as("connectorEndpoint"), + c.ONLINE_STATUS.as("connectorOnlineStatus")) + .from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) + .where(d.ASSET_ID.eq(assetId).or(d.CONNECTOR_ENDPOINT.eq(endpoint))) + .fetchOneInto(DataOfferDetailRs.class); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/ContractOfferRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java similarity index 91% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/ContractOfferRs.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java index 44b940286..f10d1dd1e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/ContractOfferRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; +package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model; import lombok.AccessLevel; import lombok.Getter; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java similarity index 90% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferRs.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java index c55fa0067..d0b6d4283 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; +package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import lombok.AccessLevel; @@ -26,7 +26,7 @@ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) -public class DataOfferRs { +public class DataOfferDetailRs { String assetId; String assetPropertiesJson; OffsetDateTime createdAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index e9061ca9c..756eb1a2a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -15,23 +15,23 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.ContractOfferRs; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferRs; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogContractOffer; +import de.sovity.edc.ext.wrapper.api.broker.model.CatalogDataOffer; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingItem; import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferListEntry; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferListEntryContractOffer; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; -import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; @RequiredArgsConstructor public class CatalogApiService { @@ -73,37 +73,37 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { result.setAvailableSortings(buildAvailableSortings()); result.setPaginationMetadata(paginationMetadata); result.setAvailableFilters(catalogFilterService.buildAvailableFilters(catalogPageRs.getAvailableFilterValues())); - result.setDataOffers(buildDataOfferListEntries(catalogPageRs.getDataOffers())); + result.setDataOffers(buildCatalogDataOffers(catalogPageRs.getDataOffers())); return result; } - private List buildDataOfferListEntries(List dataOfferRs) { + private List buildCatalogDataOffers(List dataOfferRs) { return dataOfferRs.stream() - .map(this::buildDataOfferListEntry) + .map(this::buildCatalogDataOffer) .toList(); } - private DataOfferListEntry buildDataOfferListEntry(DataOfferRs dataOfferRs) { - var dataOffer = new DataOfferListEntry(); + private CatalogDataOffer buildCatalogDataOffer(DataOfferListEntryRs dataOfferRs) { + var dataOffer = new CatalogDataOffer(); dataOffer.setAssetId(dataOfferRs.getAssetId()); dataOffer.setCreatedAt(dataOfferRs.getCreatedAt()); dataOffer.setUpdatedAt(dataOfferRs.getUpdatedAt()); dataOffer.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferRs.getAssetPropertiesJson())); - dataOffer.setContractOffers(buildDataOfferListEntryContractOffers(dataOfferRs)); + dataOffer.setContractOffers(buildCatalogContractOffers(dataOfferRs)); dataOffer.setConnectorEndpoint(dataOfferRs.getConnectorEndpoint()); dataOffer.setConnectorOfflineSinceOrLastUpdatedAt(dataOfferRs.getConnectorOfflineSinceOrLastUpdatedAt()); dataOffer.setConnectorOnlineStatus(getOnlineStatus(dataOfferRs)); return dataOffer; } - private List buildDataOfferListEntryContractOffers(DataOfferRs dataOfferRs) { + private List buildCatalogContractOffers(DataOfferListEntryRs dataOfferRs) { return dataOfferRs.getContractOffers().stream() - .map(this::buildDataOfferListEntryContractOffer) + .map(this::buildCatalogContractOffer) .toList(); } - private DataOfferListEntryContractOffer buildDataOfferListEntryContractOffer(ContractOfferRs contractOfferDbRow) { - var contractOffer = new DataOfferListEntryContractOffer(); + private CatalogContractOffer buildCatalogContractOffer(ContractOfferRs contractOfferDbRow) { + var contractOffer = new CatalogContractOffer(); contractOffer.setContractOfferId(contractOfferDbRow.getContractOfferId()); contractOffer.setContractPolicy(policyDtoBuilder.buildPolicyFromJson(contractOfferDbRow.getPolicyJson())); contractOffer.setCreatedAt(contractOfferDbRow.getCreatedAt()); @@ -111,7 +111,7 @@ private DataOfferListEntryContractOffer buildDataOfferListEntryContractOffer(Con return contractOffer; } - private ConnectorOnlineStatus getOnlineStatus(DataOfferRs dataOfferRs) { + private ConnectorOnlineStatus getOnlineStatus(DataOfferListEntryRs dataOfferRs) { return switch (dataOfferRs.getConnectorOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; @@ -120,6 +120,10 @@ private ConnectorOnlineStatus getOnlineStatus(DataOfferRs dataOfferRs) { } private static List buildAvailableSortings() { - return Arrays.stream(CatalogPageSortingType.values()).map(it -> new CatalogPageSortingItem(it, it.getTitle())).toList(); + return Stream.of( + CatalogPageSortingType.MOST_RECENT, + CatalogPageSortingType.TITLE, + CatalogPageSortingType.ORIGINATOR + ).map(it -> new CatalogPageSortingItem(it, it.getTitle())).toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 847eed0dd..096e0d5cf 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -16,6 +16,8 @@ import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageResult; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; @@ -25,9 +27,9 @@ import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; -import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; @RequiredArgsConstructor public class ConnectorApiService { @@ -46,6 +48,23 @@ public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery quer return result; } + public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDetailPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); + + var connectorDbRow = connectorPageQueryService.queryConnectorDetailPage(dsl, query.getConnectorEndpoint()); + var connector = buildConnectorListEntry(connectorDbRow); + + var result = new ConnectorDetailPageResult(); + result.setCreatedAt(connector.getCreatedAt()); + result.setEndpoint(connector.getEndpoint()); + result.setId(connector.getId()); + result.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); + result.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); + result.setNumContractOffers(connector.getNumContractOffers()); + result.setOnlineStatus(connector.getOnlineStatus()); + return result; + } + private List buildConnectorListEntries(List connectors) { return connectors.stream().map(this::buildConnectorListEntry).toList(); } @@ -71,6 +90,9 @@ private ConnectorOnlineStatus getOnlineStatus(ConnectorRs connector) { } private List buildAvailableSortings() { - return Arrays.stream(ConnectorPageSortingType.values()).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); + return Stream.of( + ConnectorPageSortingType.MOST_RECENT, + ConnectorPageSortingType.TITLE + ).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java new file mode 100644 index 000000000..1ef7c7b91 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailContractOffer; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageResult; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +public class DataOfferDetailApiService { + private final DataOfferDetailPageQueryService dataOfferDetailPageQueryService; + private final PolicyDtoBuilder policyDtoBuilder; + private final AssetPropertyParser assetPropertyParser; + + public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDetailPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); + + var dataOffer = dataOfferDetailPageQueryService.queryDataOfferDetailsPage(dsl, query.getAssetId(), query.getConnectorEndpoint()); + + var result = new DataOfferDetailPageResult(); + result.setAssetId(dataOffer.getAssetId()); + result.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); + result.setConnectorOnlineStatus(mapConnectorOnlineStatus(dataOffer.getConnectorOnlineStatus())); + result.setConnectorOfflineSinceOrLastUpdatedAt(dataOffer.getConnectorOfflineSinceOrLastUpdatedAt()); + result.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOffer.getAssetPropertiesJson())); + result.setCreatedAt(dataOffer.getCreatedAt()); + result.setUpdatedAt(dataOffer.getUpdatedAt()); + result.setContractOffers(buildDataOfferDetailContractOffers(dataOffer.getContractOffers())); + return result; + } + + private de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus mapConnectorOnlineStatus( + de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus connectorOnlineStatus + ) { + if (connectorOnlineStatus == null) { + return de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus.OFFLINE; + } + + return switch (connectorOnlineStatus) { + case ONLINE -> de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus.ONLINE; + case OFFLINE -> de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus.OFFLINE; + }; + } + + private List buildDataOfferDetailContractOffers(List contractOffers) { + return contractOffers.stream().map(this::buildDataOfferDetailContractOffer).toList(); + } + + @NotNull + private DataOfferDetailContractOffer buildDataOfferDetailContractOffer(ContractOfferRs offer) { + var newOffer = new DataOfferDetailContractOffer(); + newOffer.setCreatedAt(offer.getCreatedAt()); + newOffer.setUpdatedAt(offer.getUpdatedAt()); + newOffer.setContractOfferId(offer.getContractOfferId()); + newOffer.setContractPolicy(policyDtoBuilder.buildPolicyFromJson(offer.getPolicyJson())); + return newOffer; + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java new file mode 100644 index 000000000..781aa4504 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AssertionUtils { + @SneakyThrows + public static void assertEqualJson(String expected, String actual) { + JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 738003dde..38a55e2f6 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -15,13 +15,13 @@ package de.sovity.edc.ext.brokerserver.services.api; import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.client.gen.model.CatalogDataOffer; import de.sovity.edc.client.gen.model.CatalogPageQuery; import de.sovity.edc.client.gen.model.CatalogPageResult; import de.sovity.edc.client.gen.model.CnfFilterAttribute; import de.sovity.edc.client.gen.model.CnfFilterItem; import de.sovity.edc.client.gen.model.CnfFilterValue; import de.sovity.edc.client.gen.model.CnfFilterValueAttribute; -import de.sovity.edc.client.gen.model.DataOfferListEntry; import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; @@ -36,7 +36,6 @@ import org.eclipse.edc.policy.model.Policy; import org.jooq.DSLContext; import org.jooq.JSONB; -import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,7 +46,7 @@ import java.util.Map; import java.util.stream.IntStream; -import static de.sovity.edc.client.gen.model.DataOfferListEntry.ConnectorOnlineStatusEnum.ONLINE; +import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; import static org.assertj.core.api.Assertions.assertThat; @@ -160,7 +159,7 @@ void testDataOfferDetails() { var dataOfferResult = result.getDataOffers().get(0); assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); assertThat(dataOfferResult.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); - assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(ONLINE); + assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(CatalogDataOffer.ConnectorOnlineStatusEnum.ONLINE); assertThat(dataOfferResult.getAssetId()).isEqualTo("urn:artifact:my-asset"); assertThat(dataOfferResult.getProperties()).isEqualTo(Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset", @@ -175,12 +174,6 @@ void testDataOfferDetails() { }); } - private void assertEqualJson(String expected, String actual) { - var expectedJson = new JSONObject(expected); - var actualJson = new JSONObject(actual); - assertThat(actualJson.similar(expectedJson)).isTrue(); - } - /** * Tests against an issue where empty available filter values resulted in NULLs */ @@ -341,7 +334,7 @@ void testPagination_firstPage() { query.setSorting(CatalogPageQuery.SortingEnum.TITLE); var result = edcClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).extracting(DataOfferListEntry::getAssetId) + assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) .isEqualTo(IntStream.range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); @@ -374,7 +367,7 @@ void testPagination_secondPage() { var result = edcClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).extracting(DataOfferListEntry::getAssetId) + assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) .isEqualTo(IntStream.range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index b29c071e4..6e49c727a 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -14,22 +14,33 @@ package de.sovity.edc.ext.brokerserver.services.api; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.client.gen.model.ConnectorDetailPageQuery; import de.sovity.edc.client.gen.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.SneakyThrows; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Policy; +import org.jooq.DSLContext; +import org.jooq.JSONB; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; +import java.time.OffsetDateTime; import java.util.Map; -import static de.sovity.edc.client.gen.model.ConnectorListEntry.OnlineStatusEnum.OFFLINE; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static groovy.json.JsonOutput.toJson; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -42,18 +53,99 @@ class ConnectorApiTest { @BeforeEach void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.KNOWN_CONNECTORS, "https://example.com/ids/data" ))); } @Test void testQueryConnectors() { - var result = edcClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); - assertThat(result.getConnectors()).hasSize(1); + TEST_DATABASE.testTransaction(dsl -> { + var today = OffsetDateTime.now().withNano(0); - var connector = result.getConnectors().get(0); - assertThat(connector.getEndpoint()).isEqualTo("https://example.com/ids/data"); - assertThat(connector.getId()).isEqualTo("https://example.com"); - assertThat(connector.getOnlineStatus()).isEqualTo(OFFLINE); + createConnector(dsl, today, "http://my-connector/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" + ), "http://my-connector/ids/data"); + + var result = edcClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); + assertThat(result.getConnectors()).hasSize(1); + + var connector = result.getConnectors().get(0); + assertThat(connector.getId()).isEqualTo("http://my-connector"); + assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); + assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); + assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); + }); + } + + @Test + void testQueryConnectorDetails() { + TEST_DATABASE.testTransaction(dsl -> { + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today, "http://my-connector/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" + ), "http://my-connector/ids/data"); + + var connector = edcClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("http://my-connector/ids/data")); + assertThat(connector.getId()).isEqualTo("http://my-connector"); + assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); + assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); + assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); + }); + } + + private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setConnectorId("http://my-connector"); + connector.setEndpoint(connectorEndpoint); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setCreatedAt(today.minusDays(1)); + connector.setLastRefreshAttemptAt(today); + connector.setLastSuccessfulRefreshAt(today); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + connector.insert(); + } + + private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); + dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + dataOffer.setConnectorEndpoint(connectorEndpoint); + dataOffer.setCreatedAt(today.minusDays(5)); + dataOffer.setUpdatedAt(today); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + contractOffer.setContractOfferId("my-contract-offer-1"); + contractOffer.setConnectorEndpoint(connectorEndpoint); + contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setCreatedAt(today.minusDays(5)); + contractOffer.setUpdatedAt(today); + contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.insert(); + } + + private Policy dummyPolicy() { + return Policy.Builder.newInstance() + .assignee("Example Assignee") + .build(); + } + + private String policyToJson(Policy policy) { + return toJson(policy); + } + + @SneakyThrows + private String assetProperties(Map assetProperties) { + return new ObjectMapper().writeValueAsString(assetProperties); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java new file mode 100644 index 000000000..8d548bedc --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.client.gen.model.DataOfferDetailPageQuery; +import de.sovity.edc.client.gen.model.DataOfferDetailPageResult; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.SneakyThrows; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Policy; +import org.jooq.DSLContext; +import org.jooq.JSONB; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static groovy.json.JsonOutput.toJson; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class DataOfferDetailApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( + ))); + } + + @Test + void testQueryDataOfferDetails() { + TEST_DATABASE.testTransaction(dsl -> { + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today, "http://my-connector2/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_CATEGORY, "my-category2", + AssetProperty.ASSET_NAME, "My Asset 2" + ), "http://my-connector2/ids/data"); + + createConnector(dsl, today, "http://my-connector/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" + ), "http://my-connector/ids/data"); + + + var actual = edcClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("http://my-connector/ids/data", "urn:artifact:my-asset-1")); + + assertThat(actual.getAssetId()).isEqualTo("urn:artifact:my-asset-1"); + assertThat(actual.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(actual.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); + assertThat(actual.getConnectorOnlineStatus()).isEqualTo(DataOfferDetailPageResult.ConnectorOnlineStatusEnum.ONLINE); + assertThat(actual.getCreatedAt()).isEqualTo(today.minusDays(5)); + assertThat(actual.getProperties()).isEqualTo(Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" + )); + assertThat(actual.getUpdatedAt()).isEqualTo(today); + + assertThat(actual.getContractOffers()).hasSize(1); + var contractOffer = actual.getContractOffers().get(0); + assertThat(contractOffer.getContractOfferId()).isEqualTo("my-contract-offer-1"); + assertEqualJson(contractOffer.getContractPolicy().getLegacyPolicy(), policyToJson(dummyPolicy())); + assertThat(contractOffer.getCreatedAt()).isEqualTo(today.minusDays(5)); + assertThat(contractOffer.getUpdatedAt()).isEqualTo(today); + }); + } + + private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setConnectorId("http://my-connector"); + connector.setEndpoint(connectorEndpoint); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setCreatedAt(today.minusDays(1)); + connector.setLastRefreshAttemptAt(today); + connector.setLastSuccessfulRefreshAt(today); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + connector.insert(); + } + + private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); + dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + dataOffer.setConnectorEndpoint(connectorEndpoint); + dataOffer.setCreatedAt(today.minusDays(5)); + dataOffer.setUpdatedAt(today); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + contractOffer.setContractOfferId("my-contract-offer-1"); + contractOffer.setConnectorEndpoint(connectorEndpoint); + contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setCreatedAt(today.minusDays(5)); + contractOffer.setUpdatedAt(today); + contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.insert(); + } + + private Policy dummyPolicy() { + return Policy.Builder.newInstance() + .assignee("Example Assignee") + .build(); + } + + private String policyToJson(Policy policy) { + return toJson(policy); + } + + @SneakyThrows + private String assetProperties(Map assetProperties) { + return new ObjectMapper().writeValueAsString(assetProperties); + } +} From c267738c25bf7675f2c54b2483df088719295885 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Tue, 27 Jun 2023 17:12:17 +0200 Subject: [PATCH 075/295] feat: permanently delete old offline connectors (#151) * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * feat: permanently delete old offline connectors * chore: minor refactorings * chore: checkstyle * test: DeadConnectorRemovalTest * refactor: pr remarks * chore: checkstyle * chore: pr remarks * chore: checkstyle * refactor: test does not need full edc extension anymore --------- Co-authored-by: Richard Treier --- CHANGELOG.md | 10 +- connector/.env | 4 + .../db/migration/V4_1__MvP_1_1_0.sql | 4 + .../brokerserver/BrokerServerExtension.java | 6 + .../BrokerServerExtensionContextBuilder.java | 10 ++ .../brokerserver/dao/ConnectorQueries.java | 10 ++ .../brokerserver/dao/DataOfferQueries.java | 1 - .../services/OfflineConnectorRemover.java | 44 ++++++++ .../services/config/BrokerServerSettings.java | 2 + .../config/BrokerServerSettingsFactory.java | 15 ++- .../services/logging/BrokerEventLogger.java | 15 +++ .../schedules/OfflineConnectorRemovalJob.java | 32 ++++++ .../OfflineConnectorRemovalJobTest.java | 105 ++++++++++++++++++ 13 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0.sql create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b27e28a82..51cb051e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Patch +### Deployment Migration Notes + +1. There are new **optional** configuration properties: + ```yaml + # Deletion of Connectors after they have been offline for a certain amount of time + EDC_BROKER_SERVER_DELETE_OFFLINE_CONNECTORS_AFTER=P5D + EDC_BROKER_SERVER_SCHEDULED_DELETE_OFFLINE_CONNECTORS=0 0 12 ? * * + ## [v0.1.0] Broker MvP Release - 2023-06-23 ### Overview @@ -103,4 +111,4 @@ Please view the [Deployment Section in the README.md](README.md#deployment) for - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.0.1` - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6` -- Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) \ No newline at end of file +- Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) diff --git a/connector/.env b/connector/.env index e2b7d45dc..0071f104e 100644 --- a/connector/.env +++ b/connector/.env @@ -44,6 +44,10 @@ EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER=10 # Pagination Configuration: Catalog Page Size EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 +# Deletion of Connectors after they have been offline for a certain amount of time +EDC_BROKER_SERVER_DELETE_OFFLINE_CONNECTORS_AFTER=P5D +EDC_BROKER_SERVER_SCHEDULED_DELETE_OFFLINE_CONNECTORS=0 0 12 ? * * + # =========================================================== # Other EDC Config # =========================================================== diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0.sql new file mode 100644 index 000000000..a66d594d3 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0.sql @@ -0,0 +1,4 @@ +-- Changes to Enums are non-transactional and must be supplied in a separate migration script for flyway + +-- Connector deleted due to being offline for too long +alter type broker_event_type add value 'CONNECTOR_DELETED_DUE_TO_OFFLINE_FOR_TOO_LONG'; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 9102624ae..15a986540 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -56,6 +56,12 @@ public class BrokerServerExtension implements ServiceExtension { @Setting public static final String KNOWN_DATASPACE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS"); + @Setting + public static final String DELETE_OFFLINE_CONNECTORS_AFTER = toEdcProp("EDC_BROKER_SERVER_DELETE_OFFLINE_CONNECTORS_AFTER"); + + @Setting + public static final String SCHEDULED_DELETE_OFFLINE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_SCHEDULED_DELETE_OFFLINE_CONNECTORS"); + @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index a4cbf2325..db6e3c0d4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -30,6 +30,7 @@ import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; +import de.sovity.edc.ext.brokerserver.services.OfflineConnectorRemover; import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; @@ -57,6 +58,7 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; import de.sovity.edc.ext.brokerserver.services.schedules.ConnectorRefreshJob; +import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorRemovalJob; import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; import lombok.NoArgsConstructor; @@ -162,12 +164,19 @@ public static BrokerServerExtensionContext buildContext( var catalogFilterAttributeDefinitionService = new CatalogFilterAttributeDefinitionService(); var catalogFilterService = new CatalogFilterService(catalogFilterAttributeDefinitionService); + var offlineConnectorRemover = new OfflineConnectorRemover(brokerServerSettings, connectorQueries, brokerEventLogger); + // Schedules List> jobs = List.of( new CronJobRef<>( BrokerServerExtension.CRON_CONNECTOR_REFRESH, ConnectorRefreshJob.class, () -> new ConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ), + new CronJobRef<>( + BrokerServerExtension.SCHEDULED_DELETE_OFFLINE_CONNECTORS, + OfflineConnectorRemovalJob.class, + () -> new OfflineConnectorRemovalJob(dslContextFactory, offlineConnectorRemover) ) ); @@ -205,6 +214,7 @@ public static BrokerServerExtensionContext buildContext( catalogApiService, dataOfferDetailApiService ); + return new BrokerServerExtensionContext( brokerServerResource, brokerServerInitializer, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java index dafeef790..f1510a536 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java @@ -19,7 +19,10 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import org.jooq.DSLContext; +import java.time.Duration; +import java.time.OffsetDateTime; import java.util.Collection; +import java.util.List; import java.util.Set; public class ConnectorQueries { @@ -40,4 +43,11 @@ public Set findExistingConnectors(DSLContext dsl, Collection con .where(PostgresqlUtils.in(c.ENDPOINT, connectorEndpoints)) .fetchSet(c.ENDPOINT); } + + public List findAllConnectorsForDeletion(DSLContext dsl, Duration deleteOfflineConnectorsAfter) { + var c = Tables.CONNECTOR; + return dsl.select(c.ENDPOINT).from(c) + .where(c.LAST_SUCCESSFUL_REFRESH_AT.lt(OffsetDateTime.now().minus(deleteOfflineConnectorsAfter))) + .fetch(c.ENDPOINT); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java index 76c47582a..fed263bd0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java @@ -28,5 +28,4 @@ public List findByConnectorEndpoint(DSLContext dsl, String conn var d = Tables.DATA_OFFER; return dsl.selectFrom(d).where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); } - } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java new file mode 100644 index 000000000..30131b75d --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class OfflineConnectorRemover { + private final BrokerServerSettings brokerServerSettings; + private final ConnectorQueries connectorQueries; + private final BrokerEventLogger brokerEventLogger; + + public void removeIfOfflineTooLong(DSLContext dsl) { + var deleteOfflineConnectorsAfter = brokerServerSettings.getDeleteOfflineConnectorsAfter(); + var toDelete = connectorQueries.findAllConnectorsForDeletion(dsl, deleteOfflineConnectorsAfter); + + // delete in batches, child entities first. + dsl.deleteFrom(Tables.DATA_OFFER_CONTRACT_OFFER).where(PostgresqlUtils.in(Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, toDelete)).execute(); + dsl.deleteFrom(Tables.DATA_OFFER).where(PostgresqlUtils.in(Tables.DATA_OFFER.CONNECTOR_ENDPOINT, toDelete)).execute(); + dsl.deleteFrom(Tables.CONNECTOR).where(PostgresqlUtils.in(Tables.CONNECTOR.ENDPOINT, toDelete)).execute(); + + // add log messages + brokerEventLogger.addDeletedDueToOfflineTooLongMessages(dsl, toDelete); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java index d0fdaa67b..120cccb20 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java @@ -29,4 +29,6 @@ public class BrokerServerSettings { DataSpaceConfig dataSpaceConfig; int numThreads; + + Duration deleteOfflineConnectorsAfter; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java index d0e24c9ef..504c5ee74 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -31,16 +31,18 @@ public class BrokerServerSettingsFactory { private final Monitor monitor; public BrokerServerSettings buildBrokerServerSettings() { - var hideOfflineDataOffersAfter = getDurationOrNull(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER); + var hideOfflineDataOffersAfter = getDuration(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER, null); var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); var dataSpaceConfig = buildDataSpaceConfig(config); var numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); + var deleteOfflineConnectorsAfter = getDuration(BrokerServerExtension.DELETE_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); return BrokerServerSettings.builder() .hideOfflineDataOffersAfter(hideOfflineDataOffersAfter) .catalogPagePageSize(catalogPagePageSize) .dataSpaceConfig(dataSpaceConfig) .numThreads(numThreads) + .deleteOfflineConnectorsAfter(deleteOfflineConnectorsAfter) .build(); } @@ -76,12 +78,13 @@ private String getDefaultDataSpace(Config config) { return config.getString(BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "Default"); } - private Duration getDurationOrNull(@NonNull String configProperty) { - var durationAsString = config.getString(configProperty, ""); - if (StringUtils.isBlank(durationAsString)) { - return null; + private Duration getDuration(@NonNull String configProperty, Duration defaultValue) { + var value = config.getString(configProperty, ""); + + if (StringUtils.isBlank(value)) { + return defaultValue; } - return Duration.parse(durationAsString); + return Duration.parse(value); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index be478212f..2b9889dee 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -22,6 +22,8 @@ import org.jooq.DSLContext; import java.time.OffsetDateTime; +import java.util.List; +import java.util.stream.Collectors; /** * Updates a single connector. @@ -100,4 +102,17 @@ public void logConnectorUpdateContractOfferLimitOk(String endpoint) { logEntry.insert(); } + public void addDeletedDueToOfflineTooLongMessages(DSLContext dsl, List deletedConnectorEndpoints) { + var logEntries = deletedConnectorEndpoints.stream().map(endpoint -> { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_DELETED_DUE_TO_OFFLINE_FOR_TOO_LONG); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setUserMessage("Connector was removed for being offline too long."); + logEntry.setConnectorEndpoint(endpoint); + return logEntry; + }).collect(Collectors.toList()); + + dsl.batchInsert(logEntries).execute(); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java new file mode 100644 index 000000000..373347482 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules; + +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.services.OfflineConnectorRemover; +import lombok.RequiredArgsConstructor; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +public class OfflineConnectorRemovalJob implements Job { + private final DslContextFactory dslContextFactory; + private final OfflineConnectorRemover offlineConnectorRemover; + + @Override + public void execute(JobExecutionContext context) { + dslContextFactory.transaction(offlineConnectorRemover::removeIfOfflineTooLong); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java new file mode 100644 index 000000000..f3278a43a --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules; + +import de.sovity.edc.ext.brokerserver.TestUtils; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.services.OfflineConnectorRemover; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import org.jooq.DSLContext; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.Duration; +import java.time.OffsetDateTime; + +import static de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector.CONNECTOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class OfflineConnectorRemovalJobTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + BrokerServerSettings brokerServerSettings; + OfflineConnectorRemover offlineConnectorRemover; + + @BeforeAll + static void beforeAll() { + FlywayTestUtils.migrate(TEST_DATABASE); + } + + @BeforeEach + void beforeEach() { + brokerServerSettings = mock(BrokerServerSettings.class); + offlineConnectorRemover = new OfflineConnectorRemover( + brokerServerSettings, + new ConnectorQueries(), + new BrokerEventLogger() + ); + } + + @Test + void test_offlineConnectorRemoval_should_remove() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + when(brokerServerSettings.getDeleteOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); + createConnector(dsl, 6); + + // act + offlineConnectorRemover.removeIfOfflineTooLong(dsl); + + // assert + assertThat(dsl.selectCount().from(CONNECTOR).fetchOne(0, Integer.class)).isZero(); + }); + } + + @Test + void test_offlineConnectorRemoval_should_not_remove() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + when(brokerServerSettings.getDeleteOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); + createConnector(dsl, 2); + + // act + offlineConnectorRemover.removeIfOfflineTooLong(dsl); + + // assert + assertThat(dsl.selectCount().from(CONNECTOR).fetchOne(0, Integer.class)).isNotZero(); + }); + } + + private static void createConnector(DSLContext dsl, int createdDaysAgo) { + dsl.insertInto(CONNECTOR) + .set(CONNECTOR.CONNECTOR_ID, "http://example.org") + .set(CONNECTOR.ENDPOINT, TestUtils.MANAGEMENT_ENDPOINT) + .set(CONNECTOR.ONLINE_STATUS, ConnectorOnlineStatus.OFFLINE) + .set(CONNECTOR.LAST_SUCCESSFUL_REFRESH_AT, OffsetDateTime.now().minusDays(createdDaysAgo)) + .set(CONNECTOR.CREATED_AT, OffsetDateTime.now().minusDays(6)) + .set(CONNECTOR.DATA_OFFERS_EXCEEDED, ConnectorDataOffersExceeded.OK) + .set(CONNECTOR.CONTRACT_OFFERS_EXCEEDED, ConnectorContractOffersExceeded.OK).execute(); + } +} From 8c2b30ca5f026a916b8b3b20990ee42aac4c96a3 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 28 Jun 2023 11:28:01 +0200 Subject: [PATCH 076/295] chore: add path mapping to reverse proxy deployment documentation (#161) * chore: add path mapping to reverse proxy deployment documentation * chore: fix wording --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index afebb19c4..e6e3c3fc9 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ or if it's broken. - The UI's `80` port. - The Backend's `11002` port. - The Backend's `11003` port. +- The mapping should look like this: + - `/backend/api/v1/ids` -> `broker-backend:11003/backend/api/v1/ids` + - `/backend/api/v1/management` -> `broker-backend:11002/backend/api/v1/management` + - All other requests should be mapped to `broker-ui:80` #### Backend Configuration From 1fb1ea1b8da648db920c2d19e0e60791783e55b8 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 29 Jun 2023 09:34:37 +0200 Subject: [PATCH 077/295] fix: missing required EDC daps config params (#163) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e6e3c3fc9..a34c1d885 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' EDC_KEYSTORE: '_your keystore file_' # Needs to be available as file in the running container EDC_KEYSTORE_PASSWORD: '_your keystore password_' +EDC_OAUTH_CERTIFICATE_ALIAS: 1 +EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 # Required: Management API Key EDC_API_AUTH_KEY: "ApiKeyDefaultValue" From 72c2e89611efeaee7d6172a57a82e2fcf0552bb5 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 29 Jun 2023 16:36:23 +0200 Subject: [PATCH 078/295] refactor: split api wrapper (#162) Co-authored-by: Richard Treier --- .../build-and-publish-ts-api-client.yml | 65 + build.gradle.kts | 2 +- connector/build.gradle.kts | 3 - extensions/broker-server-api/README.md | 27 + .../broker-server-api/api/build.gradle.kts | 89 + .../api/BrokerServerResource.java | 64 + .../api/model/CatalogContractOffer.java | 46 + .../api/model/CatalogDataOffer.java | 59 + .../api/model/CatalogPageQuery.java | 43 + .../api/model/CatalogPageResult.java | 45 + .../api/model/CatalogPageSortingItem.java | 36 + .../api/model/CatalogPageSortingType.java | 32 + .../ext/brokerserver/api/model/CnfFilter.java | 38 + .../api/model/CnfFilterAttribute.java | 40 + .../brokerserver/api/model/CnfFilterItem.java | 36 + .../api/model/CnfFilterValue.java | 36 + .../api/model/CnfFilterValueAttribute.java | 38 + .../api/model/ConnectorDetailPageQuery.java | 34 + .../api/model/ConnectorDetailPageResult.java | 53 + .../api/model/ConnectorListEntry.java | 54 + .../api/model/ConnectorOnlineStatus.java | 24 + .../api/model/ConnectorPageQuery.java | 40 + .../api/model/ConnectorPageResult.java | 41 + .../api/model/ConnectorPageSortingItem.java | 36 + .../api/model/ConnectorPageSortingType.java | 30 + .../model/DataOfferDetailContractOffer.java | 46 + .../api/model/DataOfferDetailPageQuery.java | 37 + .../api/model/DataOfferDetailPageResult.java | 58 + .../api/model/PaginationMetadata.java | 43 + .../broker-server-api/client-ts/.gitignore | 24 + .../client-ts/.prettierignore | 2 + .../broker-server-api/client-ts/index.html | 12 + .../client-ts/package-lock.json | 2559 +++++++++++++++++ .../broker-server-api/client-ts/package.json | 55 + .../client-ts/prettier.config.cjs | 26 + .../client-ts/src/BrokerServerClient.ts | 40 + .../client-ts/src/generated/.gitignore | 2 + .../broker-server-api/client-ts/src/index.ts | 2 + .../client-ts/src/vite-env.d.ts | 1 + .../broker-server-api/client-ts/tsconfig.json | 19 + .../client-ts/vite.config.ts | 17 + .../build.gradle.kts | 5 +- extensions/broker-server/build.gradle.kts | 6 +- .../BrokerServerExtensionContext.java | 2 +- .../BrokerServerResourceImpl.java | 18 +- .../catalog/CatalogQueryDataOfferFetcher.java | 2 +- .../pages/catalog/CatalogQueryService.java | 2 +- .../catalog/CatalogQuerySortingService.java | 2 +- .../connector/ConnectorPageQueryService.java | 2 +- .../services/api/CatalogApiService.java | 14 +- .../services/api/ConnectorApiService.java | 16 +- .../api/DataOfferDetailApiService.java | 15 +- .../services/api/PaginationMetadataUtils.java | 2 +- .../api/filtering/CatalogFilterService.java | 10 +- .../edc/ext/brokerserver/TestUtils.java | 2 +- gradle.properties | 2 +- gradlew | 0 settings.gradle.kts | 1 + 58 files changed, 4000 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/build-and-publish-ts-api-client.yml create mode 100644 extensions/broker-server-api/README.md create mode 100644 extensions/broker-server-api/api/build.gradle.kts create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java create mode 100644 extensions/broker-server-api/client-ts/.gitignore create mode 100644 extensions/broker-server-api/client-ts/.prettierignore create mode 100644 extensions/broker-server-api/client-ts/index.html create mode 100644 extensions/broker-server-api/client-ts/package-lock.json create mode 100644 extensions/broker-server-api/client-ts/package.json create mode 100644 extensions/broker-server-api/client-ts/prettier.config.cjs create mode 100644 extensions/broker-server-api/client-ts/src/BrokerServerClient.ts create mode 100644 extensions/broker-server-api/client-ts/src/generated/.gitignore create mode 100644 extensions/broker-server-api/client-ts/src/index.ts create mode 100644 extensions/broker-server-api/client-ts/src/vite-env.d.ts create mode 100644 extensions/broker-server-api/client-ts/tsconfig.json create mode 100644 extensions/broker-server-api/client-ts/vite.config.ts mode change 100644 => 100755 gradlew diff --git a/.github/workflows/build-and-publish-ts-api-client.yml b/.github/workflows/build-and-publish-ts-api-client.yml new file mode 100644 index 000000000..9e6d16e31 --- /dev/null +++ b/.github/workflows/build-and-publish-ts-api-client.yml @@ -0,0 +1,65 @@ +name: TypeScript API Client Library + +on: + push: + branches: [ main ] + release: + types: [ published ] + pull_request: + branches: [ main ] + +jobs: + build-and-publish-npm-package: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + + steps: + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + cache-dependency-path: ./extensions/broker-server-api/client-ts/package-lock.json + - name: Generate openapi.yaml & Client Code + run: | + ./gradlew :extensions:broker-server-api:api:clean :extensions:broker-server-api:api:build -x test --no-daemon + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: NPM Package Dist Tag & Version + working-directory: ./extensions/broker-server-api/client-ts + run: | + if [ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]; then + # Full Release + VERSION="${GITHUB_REF#refs/tags/v}" + DIST_TAG=latest + else + VERSION="0.$(date '+%Y%m%d.%H%M%S')-main-$CI_SHA_SHORT" + DIST_TAG=main + fi + npm version $VERSION + echo "DIST_TAG=$DIST_TAG" >> $GITHUB_ENV + - name: Build NPM Library + working-directory: ./extensions/broker-server-api/client-ts + run: | + npm ci && npm run build + - name: Publish NPM Library + if: github.ref == 'refs/heads/main' || github.ref == 'refs/tags/v*' + working-directory: ./extensions/broker-server-api/client-ts + run: | + npm set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN + npm set //registry.npmjs.org/:username $NODE_USER + npm publish --access public --tag "${{ env.DIST_TAG }}" + env: + NODE_USER: richardtreier-sovity + NODE_AUTH_TOKEN: ${{ secrets.SOVITY_EDC_CLIENT_NPM_AUTH }} diff --git a/build.gradle.kts b/build.gradle.kts index 4cae5a540..6bcd36515 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,7 +44,7 @@ allprojects { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } maven { - name = "GitHubPackages" + name = "Github-EDC-Extensions" url = uri("https://maven.pkg.github.com/sovity/edc-extensions") credentials { username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index 0ae99e7b7..d373b592a 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -7,9 +7,6 @@ plugins { val edcVersion: String by project val edcGroup: String by project -val sovityEdcExtensionGroup: String by project -val sovityEdcExtensionsVersion: String by project - dependencies { // Control-Plane implementation("${edcGroup}:control-plane-core:${edcVersion}") diff --git a/extensions/broker-server-api/README.md b/extensions/broker-server-api/README.md new file mode 100644 index 000000000..b139b827d --- /dev/null +++ b/extensions/broker-server-api/README.md @@ -0,0 +1,27 @@ + +
+

+ + Logo + + +

Broker Server / Broker UI API Specification

+ +

+ Report Bug + · + Request Feature +

+
+ +## About this component + +Specification of Broker Server API endpoints as required for the Broker UI. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE.md) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts new file mode 100644 index 000000000..6a6983cf8 --- /dev/null +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -0,0 +1,89 @@ +val sovityEdcGroup: String by project +val sovityEdcExtensionsVersion: String by project + +plugins { + `java-library` + `maven-publish` + id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.14" //./gradlew clean resolve + id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI + id("org.openapi.generator") version "6.6.0" //./gradlew openApiValidate && ./gradlew openApiGenerate +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:1.18.28") + compileOnly("org.projectlombok:lombok:1.18.28") + + api("${sovityEdcGroup}:wrapper-common-api:${sovityEdcExtensionsVersion}") + + api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + api("jakarta.validation:jakarta.validation-api:3.0.2") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.14") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.14") + api("jakarta.servlet:jakarta.servlet-api:5.0.0") + + implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.14") + implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.14") + implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation("org.apache.commons:commons-lang3:3.12.0") +} + +val openapiFileDir = "${project.buildDir}/swagger" +val openapiFileFilename = "broker-server.yaml" +val openapiFile = "$openapiFileDir/$openapiFileFilename" + +tasks.withType { + outputDir = file(openapiFileDir) + outputFileName = openapiFileFilename.removeSuffix(".yaml") + prettyPrint = true + outputFormat = io.swagger.v3.plugins.gradle.tasks.ResolveTask.Format.YAML + classpath = java.sourceSets["main"].runtimeClasspath + buildClasspath = classpath + resourcePackages = setOf("") +} + +task("openApiGenerateTypeScriptClient") { + validateSpec.set(false) + dependsOn("resolve") + generatorName.set("typescript-fetch") + configOptions.set(mutableMapOf( + "supportsES6" to "true", + "npmVersion" to "8.15.0", + "typescriptThreePlus" to "true", + )) + + inputSpec.set(openapiFile) + val outputDirectory = buildFile.parentFile.resolve("../client-ts/src/generated").normalize() + outputDir.set(outputDirectory.toString()) + + doFirst { + project.delete(fileTree(outputDirectory).exclude("**/.gitignore")) + } + + doLast { + outputDirectory.resolve("src/generated").renameTo(outputDirectory) + } +} + +tasks.withType { + dependsOn("resolve") + dependsOn("openApiGenerateTypeScriptClient") + from(openapiFileDir) { + include(openapiFileFilename) + } +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java new file mode 100644 index 000000000..0efd097ba --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api; + +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("wrapper/broker") +@Tag(name = "Broker Server", description = "Broker Server API Endpoints. Requires the Broker Server Extension") +public interface BrokerServerResource { + + @POST + @Path("catalog-page") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Query the Broker's Catalog of Data Offers") + CatalogPageResult catalogPage(CatalogPageQuery query); + + @POST + @Path("connector-page") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Query the List of Known Connectors") + ConnectorPageResult connectorPage(ConnectorPageQuery query); + + @POST + @Path("data-offer-detail-page") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Query a Data Offer's Detail Page") + DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery query); + + @POST + @Path("connector-detail-page") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Query a Known Connector's Detail Page") + ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query); +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java new file mode 100644 index 000000000..87cebdb94 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "A contract offer a data offer is available under (as required by the catalog).") +public class CatalogContractOffer { + @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractOfferId; + + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime updatedAt; + + @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) + private PolicyDto contractPolicy; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java new file mode 100644 index 000000000..44bcd3782 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Data Offer, meaning an offered asset.") +public class CatalogDataOffer { + @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetId; + + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + + @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorOnlineStatus connectorOnlineStatus; + + @Schema(description = "Date to be displayed as last update date, for online connectors it's the last refresh date, for offline connectors it's the creation date or last successful fetch.") + private OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; + + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime updatedAt; + + @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) + private Map properties; + + @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) + private List contractOffers; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java new file mode 100644 index 000000000..a69f19e37 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Filterable Catalog Page Query") +public class CatalogPageQuery { + @Schema(description = "Selected filters") + private CnfFilterValue filter; + + @Schema(description = "Search query") + private String searchQuery; + + @Schema(description = "Sorting") + private CatalogPageSortingType sorting; + + @Schema(description = "Page number, one based, meaning the first page is page 1.", example = "1", defaultValue = "1", type = "n") + private Integer pageOneBased; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java new file mode 100644 index 000000000..5b99be10e --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Catalog Page and visible filters") +public class CatalogPageResult { + @Schema(description = "Available filter options", requiredMode = Schema.RequiredMode.REQUIRED) + private CnfFilter availableFilters; + + @Schema(description = "Available sorting options", requiredMode = Schema.RequiredMode.REQUIRED) + private List availableSortings; + + @Schema(description = "Pagination Metadata", requiredMode = Schema.RequiredMode.REQUIRED) + private PaginationMetadata paginationMetadata; + + @Schema(description = "Current page of data offers", requiredMode = Schema.RequiredMode.REQUIRED) + private List dataOffers; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java new file mode 100644 index 000000000..ea1d1ed7d --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Available Catalog Page Sorting Item") +public class CatalogPageSortingItem { + @Schema(description = "Sorting ID", requiredMode = Schema.RequiredMode.REQUIRED) + private CatalogPageSortingType sorting; + @Schema(description = "Sorting Title", example = "By Relevance", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java new file mode 100644 index 000000000..38e404bb5 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(description = "Catalog's sorting options") +public enum CatalogPageSortingType { + VIEW_COUNT("By Popularity"), + MOST_RECENT("Most Recent"), + TITLE("By Title"), + ORIGINATOR("By Connector"); + + private final String title; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java new file mode 100644 index 000000000..54c9395c5 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Filter in form of a conjunctive normal form, meaning (A=X OR A=Y) AND (B=M or B=N). " + + "Not selected attributes default to TRUE. Used here to let the backend be a SSOT for the available filter options, " + + "e.g. Transport Mode, Data Model, etc.") +public class CnfFilter { + @Schema(description = "Available attributes to filter by.", requiredMode = Schema.RequiredMode.REQUIRED) + private List fields; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java new file mode 100644 index 000000000..72687dde4 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Attribute, e.g. Language") +public class CnfFilterAttribute { + @Schema(description = "Attribute ID", example = "asset:prop:language", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + @Schema(description = "Attribute Title", example = "Language", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + @Schema(description = "Available values.", requiredMode = Schema.RequiredMode.REQUIRED) + private List values; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java new file mode 100644 index 000000000..1b6489382 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Attribute Value") +public class CnfFilterItem { + @Schema(description = "Value ID", example = "https://w3id.org/idsa/code/EN", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + @Schema(description = "Value Title", example = "English", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java new file mode 100644 index 000000000..eb73b0449 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Cnf filter's selected value.") +public class CnfFilterValue { + @Schema(description = "Available attributes to filter by.", requiredMode = Schema.RequiredMode.REQUIRED) + private List selectedAttributeValues; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java new file mode 100644 index 000000000..a0ab4fcd1 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Single attribute of selected cnf filter's value") +public class CnfFilterValueAttribute { + @Schema(description = "Attribute ID", example = "asset:prop:language", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + @Schema(description = "Selected attribute values' IDs.", requiredMode = Schema.RequiredMode.REQUIRED) + private List selectedIds; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java new file mode 100644 index 000000000..e43d2e273 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Connector Page Detail Query") +public class ConnectorDetailPageQuery { + @Schema(description = "Connector Endpoint1") + private String connectorEndpoint; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java new file mode 100644 index 000000000..9cc08d915 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Connector Detail Page Data") +public class ConnectorDetailPageResult { + @Schema(description = "Connector ID", example = "https://my-test.connector", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + private String endpoint; + + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Last time the connector was successfully refreshed.") + private OffsetDateTime lastSuccessfulRefreshAt; + + @Schema(description = "Last time the connector was tried to be refreshed.") + private OffsetDateTime lastRefreshAttemptAt; + + @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorOnlineStatus onlineStatus; + + @Schema(description = "Number of known data offerings") + private Integer numContractOffers; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java new file mode 100644 index 000000000..42248d5c5 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "A Contract Offer's Connector Status") +public class ConnectorListEntry { + @Schema(description = "Connector ID", example = "https://my-test.connector", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + private String endpoint; + + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Last time the connector was successfully refreshed.") + private OffsetDateTime lastSuccessfulRefreshAt; + + @Schema(description = "Last time the connector was tried to be refreshed.") + private OffsetDateTime lastRefreshAttemptAt; + + @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorOnlineStatus onlineStatus; + + @Schema(description = "Number of known data offerings") + private Integer numContractOffers; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java new file mode 100644 index 000000000..43700aea5 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Connector's online status") +public enum ConnectorOnlineStatus { + ONLINE, + OFFLINE +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java new file mode 100644 index 000000000..38f3afad6 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Filterable Connector Page Query") +public class ConnectorPageQuery { + @Schema(description = "Search query") + private String searchQuery; + + @Schema(description = "Sorting") + private ConnectorPageSortingType sorting; + + @Schema(description = "Page number, one based, meaning the first page is page 1.", example = "1", defaultValue = "1", type = "n") + private Integer pageOneBased; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java new file mode 100644 index 000000000..247a701c0 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Connector Page Data") +public class ConnectorPageResult { + @Schema(description = "Available sorting options", requiredMode = Schema.RequiredMode.REQUIRED) + private List availableSortings; + + @Schema(description = "Pagination Metadata", requiredMode = Schema.RequiredMode.REQUIRED) + private PaginationMetadata paginationMetadata; + + @Schema(description = "Current page of connector list entries", requiredMode = Schema.RequiredMode.REQUIRED) + private List connectors; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java new file mode 100644 index 000000000..1849de2fb --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Available Connector Page Sorting Item") +public class ConnectorPageSortingItem { + @Schema(description = "Sorting ID", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorPageSortingType sorting; + @Schema(description = "Sorting Title", example = "Alphabetically", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java new file mode 100644 index 000000000..f110a5c6b --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(description = "Connector List Page's known sorting option IDs") +public enum ConnectorPageSortingType { + MOST_RECENT("Most Recent"), + TITLE("By Title"); + + private final String title; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java new file mode 100644 index 000000000..6803d2293 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "A contract offer a data offer is available under (as required by the data offer detail page).") +public class DataOfferDetailContractOffer { + @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractOfferId; + + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime updatedAt; + + @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) + private PolicyDto contractPolicy; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java new file mode 100644 index 000000000..7dc00ff72 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Data Offer Detail Page Query") +public class DataOfferDetailPageQuery { + @Schema(description = "Connector Endpoint") + private String connectorEndpoint; + + @Schema(description = "Asset ID") + private String assetId; +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java new file mode 100644 index 000000000..96a31edb2 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Data Offer Detail Page.") +public class DataOfferDetailPageResult { + @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetId; + + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + + @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorOnlineStatus connectorOnlineStatus; + + @Schema(description = "Date to be displayed as last update date, for online connectors it's the last refresh date, for offline connectors it's the creation date or last successful fetch.") + private OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; + + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime updatedAt; + + @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) + private Map properties; + + @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) + private List contractOffers; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java new file mode 100644 index 000000000..4edbebbfe --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Pagination Metadata") +public class PaginationMetadata { + @Schema(description = "Total number of results", example = "368", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer numTotal; + + @Schema(description = "Visible number of results", example = "20", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer numVisible; + + @Schema(description = "Page number, one based, meaning the first page is page 1.", example = "1", defaultValue = "1", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer pageOneBased; + + @Schema(description = "Items per page", example = "20", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer pageSize; +} + diff --git a/extensions/broker-server-api/client-ts/.gitignore b/extensions/broker-server-api/client-ts/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/extensions/broker-server-api/client-ts/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/extensions/broker-server-api/client-ts/.prettierignore b/extensions/broker-server-api/client-ts/.prettierignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/extensions/broker-server-api/client-ts/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/extensions/broker-server-api/client-ts/index.html b/extensions/broker-server-api/client-ts/index.html new file mode 100644 index 000000000..f49a7cf04 --- /dev/null +++ b/extensions/broker-server-api/client-ts/index.html @@ -0,0 +1,12 @@ + + + + + + Client example + + +
+ + + diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json new file mode 100644 index 000000000..fa8287984 --- /dev/null +++ b/extensions/broker-server-api/client-ts/package-lock.json @@ -0,0 +1,2559 @@ +{ + "name": "@sovity.de/broker-server-client", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@sovity.de/broker-server-client", + "version": "0.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^18.15.11", + "prettier": "^2.8.7", + "typescript": "^4.9.3", + "vite": "^4.2.0", + "vite-plugin-dts": "^2.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.15", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz", + "integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.34.4.tgz", + "integrity": "sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz", + "integrity": "sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "3.55.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", + "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "dev": true, + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", + "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "dev": true, + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", + "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "dev": true, + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.1.1.tgz", + "integrity": "sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.17.3", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@ts-morph/common": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.18.1.tgz", + "integrity": "sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^5.1.0", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.17.15", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", + "integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.15", + "@esbuild/android-arm64": "0.17.15", + "@esbuild/android-x64": "0.17.15", + "@esbuild/darwin-arm64": "0.17.15", + "@esbuild/darwin-x64": "0.17.15", + "@esbuild/freebsd-arm64": "0.17.15", + "@esbuild/freebsd-x64": "0.17.15", + "@esbuild/linux-arm": "0.17.15", + "@esbuild/linux-arm64": "0.17.15", + "@esbuild/linux-ia32": "0.17.15", + "@esbuild/linux-loong64": "0.17.15", + "@esbuild/linux-mips64el": "0.17.15", + "@esbuild/linux-ppc64": "0.17.15", + "@esbuild/linux-riscv64": "0.17.15", + "@esbuild/linux-s390x": "0.17.15", + "@esbuild/linux-x64": "0.17.15", + "@esbuild/netbsd-x64": "0.17.15", + "@esbuild/openbsd-x64": "0.17.15", + "@esbuild/sunos-x64": "0.17.15", + "@esbuild/win32-arm64": "0.17.15", + "@esbuild/win32-ia32": "0.17.15", + "@esbuild/win32-x64": "0.17.15" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kolorist": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.7.0.tgz", + "integrity": "sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", + "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-morph": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-17.0.1.tgz", + "integrity": "sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.18.0", + "code-block-writer": "^11.0.3" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validator": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz", + "integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.18.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.2.0.tgz", + "integrity": "sha512-XmZtv02I7eGWm3DrZbLo1AdJK5gCisk9GqJBpY4N63pDYR6AVUnlyjFP5FCBvSBUfgE0Ppl90bKgtJU9k3AzFw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@microsoft/api-extractor": "^7.33.5", + "@rollup/pluginutils": "^5.0.2", + "@rushstack/node-core-library": "^3.53.2", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "kolorist": "^1.6.0", + "magic-string": "^0.29.0", + "ts-morph": "17.0.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": ">=2.9.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + }, + "dependencies": { + "@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + }, + "dependencies": { + "@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "dev": true + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "dependencies": { + "@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@esbuild/win32-x64": { + "version": "0.17.15", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz", + "integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==", + "dev": true, + "optional": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@microsoft/api-extractor": { + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.34.4.tgz", + "integrity": "sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "dependencies": { + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz", + "integrity": "sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@rushstack/node-core-library": { + "version": "3.55.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", + "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "dev": true, + "requires": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", + "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "dev": true, + "requires": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "@rushstack/ts-command-line": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", + "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@trivago/prettier-plugin-sort-imports": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.1.1.tgz", + "integrity": "sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==", + "dev": true, + "requires": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.17.3", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + } + }, + "@ts-morph/common": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.18.1.tgz", + "integrity": "sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA==", + "dev": true, + "requires": { + "fast-glob": "^3.2.12", + "minimatch": "^5.1.0", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "@types/node": { + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "esbuild": { + "version": "0.17.15", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", + "integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.15", + "@esbuild/android-arm64": "0.17.15", + "@esbuild/android-x64": "0.17.15", + "@esbuild/darwin-arm64": "0.17.15", + "@esbuild/darwin-x64": "0.17.15", + "@esbuild/freebsd-arm64": "0.17.15", + "@esbuild/freebsd-x64": "0.17.15", + "@esbuild/linux-arm": "0.17.15", + "@esbuild/linux-arm64": "0.17.15", + "@esbuild/linux-ia32": "0.17.15", + "@esbuild/linux-loong64": "0.17.15", + "@esbuild/linux-mips64el": "0.17.15", + "@esbuild/linux-ppc64": "0.17.15", + "@esbuild/linux-riscv64": "0.17.15", + "@esbuild/linux-s390x": "0.17.15", + "@esbuild/linux-x64": "0.17.15", + "@esbuild/netbsd-x64": "0.17.15", + "@esbuild/openbsd-x64": "0.17.15", + "@esbuild/sunos-x64": "0.17.15", + "@esbuild/win32-arm64": "0.17.15", + "@esbuild/win32-ia32": "0.17.15", + "@esbuild/win32-x64": "0.17.15" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "kolorist": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.7.0.tgz", + "integrity": "sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rollup": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", + "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-morph": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-17.0.1.tgz", + "integrity": "sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g==", + "dev": true, + "requires": { + "@ts-morph/common": "~0.18.0", + "code-block-writer": "^11.0.3" + } + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "validator": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "dev": true + }, + "vite": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz", + "integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.18.0" + } + }, + "vite-plugin-dts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.2.0.tgz", + "integrity": "sha512-XmZtv02I7eGWm3DrZbLo1AdJK5gCisk9GqJBpY4N63pDYR6AVUnlyjFP5FCBvSBUfgE0Ppl90bKgtJU9k3AzFw==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.15", + "@microsoft/api-extractor": "^7.33.5", + "@rollup/pluginutils": "^5.0.2", + "@rushstack/node-core-library": "^3.53.2", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "kolorist": "^1.6.0", + "magic-string": "^0.29.0", + "ts-morph": "17.0.1" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "requires": { + "commander": "^9.4.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } + } + } +} diff --git a/extensions/broker-server-api/client-ts/package.json b/extensions/broker-server-api/client-ts/package.json new file mode 100644 index 000000000..101326fbf --- /dev/null +++ b/extensions/broker-server-api/client-ts/package.json @@ -0,0 +1,55 @@ +{ + "name": "@sovity.de/broker-server-client", + "version": "0.0.0", + "description": "TypeScript API Client for the Broker Server developed by sovity.", + "author": "sovity GmbH", + "license": "Apache-2.0", + "homepage": "https://sovity.de", + "repository": { + "type": "git", + "url": "https://github.com/sovity/edc-broker-server-extension/" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "bugs": { + "url": "https://github.com/sovity/edc-broker-server-extension/issues/new/choose" + }, + "keywords": [ + "sovity", + "api client", + "edc", + "eclipse dataspace components", + "mobility data space", + "Catena-X", + "Mobilithek", + "broker" + ], + "type": "module", + "main": "./dist/sovity-broker-server-client.umd.cjs", + "module": "./dist/sovity-broker-server-client.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/sovity-broker-server-client.js", + "require": "./dist/sovity-broker-server-client.umd.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "npm run format-all && tsc && vite build", + "preview": "vite preview", + "format-all": "prettier --write ." + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^18.15.11", + "prettier": "^2.8.7", + "typescript": "^4.9.3", + "vite": "^4.2.0", + "vite-plugin-dts": "^2.2.0" + } +} diff --git a/extensions/broker-server-api/client-ts/prettier.config.cjs b/extensions/broker-server-api/client-ts/prettier.config.cjs new file mode 100644 index 000000000..fbcf1dc96 --- /dev/null +++ b/extensions/broker-server-api/client-ts/prettier.config.cjs @@ -0,0 +1,26 @@ +module.exports = { + tabWidth: 4, + useTabs: false, + singleQuote: true, + semi: true, + arrowParens: 'always', + trailingComma: 'all', + bracketSameLine: true, + printWidth: 80, + bracketSpacing: false, + proseWrap: 'always', + + // @trivago/prettier-plugin-sort-imports + importOrder: [ + '', + // rest after + '^[./]', + ], + importOrderParserPlugins: [ + 'typescript', + 'classProperties', + 'decorators-legacy', + ], + importOrderSeparation: false, + importOrderSortSpecifiers: true, +}; diff --git a/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts b/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts new file mode 100644 index 000000000..2dd606cc1 --- /dev/null +++ b/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts @@ -0,0 +1,40 @@ +import { + BrokerServerApi, + Configuration, + ConfigurationParameters +} from './generated'; + +/** + * API Client for our sovity Broker Server Client + */ +export interface BrokerServerClient { + brokerServerApi: BrokerServerApi; +} + +/** + * Configure & Build new Broker Server Client + * @param opts opts + */ +export function buildBrokerServerClient(opts: BrokerServerClientOptions): BrokerServerClient { + const config = new Configuration({ + basePath: opts.managementApiUrl, + headers: { + 'x-api-key': opts.managementApiKey ?? 'ApiKeyDefaultValue', + }, + credentials: 'same-origin', + ...opts.configOverrides, + }); + + return { + brokerServerApi: new BrokerServerApi(config), + }; +} + +/** + * Options for instantiating an EDC API Client + */ +export interface BrokerServerClientOptions { + managementApiUrl: string; + managementApiKey?: string; + configOverrides?: Partial; +} diff --git a/extensions/broker-server-api/client-ts/src/generated/.gitignore b/extensions/broker-server-api/client-ts/src/generated/.gitignore new file mode 100644 index 000000000..654617bb0 --- /dev/null +++ b/extensions/broker-server-api/client-ts/src/generated/.gitignore @@ -0,0 +1,2 @@ +**/* +!.gitignore diff --git a/extensions/broker-server-api/client-ts/src/index.ts b/extensions/broker-server-api/client-ts/src/index.ts new file mode 100644 index 000000000..861cb71d7 --- /dev/null +++ b/extensions/broker-server-api/client-ts/src/index.ts @@ -0,0 +1,2 @@ +export * from './BrokerServerClient'; +export * from './generated'; diff --git a/extensions/broker-server-api/client-ts/src/vite-env.d.ts b/extensions/broker-server-api/client-ts/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/extensions/broker-server-api/client-ts/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/extensions/broker-server-api/client-ts/tsconfig.json b/extensions/broker-server-api/client-ts/tsconfig.json new file mode 100644 index 000000000..138755d01 --- /dev/null +++ b/extensions/broker-server-api/client-ts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/extensions/broker-server-api/client-ts/vite.config.ts b/extensions/broker-server-api/client-ts/vite.config.ts new file mode 100644 index 000000000..acfae8c24 --- /dev/null +++ b/extensions/broker-server-api/client-ts/vite.config.ts @@ -0,0 +1,17 @@ +// noinspection JSUnusedGlobalSymbols +import {resolve} from 'path'; +import {defineConfig} from 'vite'; +import dts from 'vite-plugin-dts'; + +// https://vitejs.dev/guide/build.html#library-mode +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'broker-server-client', + fileName: 'broker-server-client', + }, + sourcemap: true, + }, + plugins: [dts()], +}); diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 6413177d4..131bae8d4 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -188,13 +188,10 @@ tasks.withType { } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup - publishing { publications { create(project.name) { from(components["java"]) } } -} \ No newline at end of file +} diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index ba769dec5..427630cc4 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -8,10 +8,10 @@ val jupiterVersion: String by project val mockitoVersion: String by project val assertj: String by project val okHttpVersion: String by project -val sovityEdcGroup: String by project -val sovityEdcExtensionsVersion: String by project val restAssured: String by project val testcontainersVersion: String by project +val sovityEdcGroup: String by project +val sovityEdcExtensionsVersion: String by project configurations.all { resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) @@ -26,7 +26,7 @@ dependencies { implementation("${edcGroup}:management-api-configuration:${edcVersion}") api(project(":extensions:broker-server-postgres-flyway-jooq")) - api("${sovityEdcGroup}:wrapper-broker-api:${sovityEdcExtensionsVersion}") { isChanging = true } + api(project(":extensions:broker-server-api:api")) implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index 538971fe0..f9010ec1d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -14,10 +14,10 @@ package de.sovity.edc.ext.brokerserver; +import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; -import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; /** diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index db4604218..c24f42ed6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -14,19 +14,19 @@ package de.sovity.edc.ext.brokerserver; +import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; -import de.sovity.edc.ext.wrapper.api.broker.BrokerServerResource; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageResult; import lombok.RequiredArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java index dc978033e..771ad908d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -14,11 +14,11 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; import org.jooq.Field; import org.jooq.Record; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java index 3a57981aa..f1e46034b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java @@ -14,12 +14,12 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogPageRs; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java index a95fe3db6..d3661286a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jooq.OrderField; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java index 7c766311e..ad22a2db4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java @@ -14,11 +14,11 @@ package de.sovity.edc.ext.brokerserver.dao.pages.connector; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.jooq.Field; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index 756eb1a2a..c25e32644 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -19,13 +19,13 @@ import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogContractOffer; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogDataOffer; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingItem; -import de.sovity.edc.ext.wrapper.api.broker.model.CatalogPageSortingType; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.api.model.CatalogContractOffer; +import de.sovity.edc.ext.brokerserver.api.model.CatalogDataOffer; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingItem; +import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 096e0d5cf..146f4e291 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -16,14 +16,14 @@ import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorDetailPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorListEntry; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageResult; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingItem; -import de.sovity.edc.ext.wrapper.api.broker.model.ConnectorPageSortingType; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorListEntry; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingItem; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java index 1ef7c7b91..34ac2cfd0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java @@ -14,11 +14,12 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailContractOffer; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.DataOfferDetailPageResult; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailContractOffer; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -49,16 +50,16 @@ public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDe return result; } - private de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus mapConnectorOnlineStatus( + private ConnectorOnlineStatus mapConnectorOnlineStatus( de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus connectorOnlineStatus ) { if (connectorOnlineStatus == null) { - return de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus.OFFLINE; + return ConnectorOnlineStatus.OFFLINE; } return switch (connectorOnlineStatus) { - case ONLINE -> de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus.ONLINE; - case OFFLINE -> de.sovity.edc.ext.wrapper.api.broker.model.ConnectorOnlineStatus.OFFLINE; + case ONLINE -> ConnectorOnlineStatus.ONLINE; + case OFFLINE -> ConnectorOnlineStatus.OFFLINE; }; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java index e88a80fcb..16ed30725 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java @@ -15,7 +15,7 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; -import de.sovity.edc.ext.wrapper.api.broker.model.PaginationMetadata; +import de.sovity.edc.ext.brokerserver.api.model.PaginationMetadata; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index c653b041d..cc68b5ff6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -19,11 +19,11 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; -import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilter; -import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterAttribute; -import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterItem; -import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterValue; -import de.sovity.edc.ext.wrapper.api.broker.model.CnfFilterValueAttribute; +import de.sovity.edc.ext.brokerserver.api.model.CnfFilter; +import de.sovity.edc.ext.brokerserver.api.model.CnfFilterAttribute; +import de.sovity.edc.ext.brokerserver.api.model.CnfFilterItem; +import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValue; +import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValueAttribute; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 71c94fcfc..6f6e21d79 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 sovity GmbH + * Copyright (c) 2023 sovity GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at diff --git a/gradle.properties b/gradle.properties index cce85f653..434b66b96 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT -# Sovity EDC Extensions +# Sovity EDC Extensions (for policy always true) sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle.kts b/settings.gradle.kts index d5f3d7136..4dfb2a8ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,5 @@ rootProject.name = "edc-broker-server-extension" include(":extensions:broker-server") include(":extensions:broker-server-postgres-flyway-jooq") +include(":extensions:broker-server-api:api") include(":connector") From 763227f19360cc5596faccfa2c2d27ce0642ac25 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Fri, 30 Jun 2023 10:17:19 +0200 Subject: [PATCH 079/295] fix: api wrapper integration (#169) * fix: api wrapper integration * fix: api wrapper integration * chore: checkstyle * chore: checkstyle --- .../ext/brokerserver/api/ApiInformation.java | 44 +++++ extensions/broker-server-api/client/README.md | 107 +++++++++++ .../broker-server-api/client/build.gradle.kts | 132 ++++++++++++++ .../java/de/sovity/edc/client/EdcClient.java | 32 ++++ .../sovity/edc/client/EdcClientBuilder.java | 44 +++++ .../sovity/edc/client/EdcClientFactory.java | 57 ++++++ .../oauth2/OAuth2ClientCredentials.java | 36 ++++ .../OAuth2CredentialsAuthenticator.java | 59 ++++++ .../oauth2/OAuth2CredentialsInterceptor.java | 40 ++++ .../client/oauth2/OAuth2CredentialsStore.java | 54 ++++++ .../edc/client/oauth2/OAuth2TokenFetcher.java | 56 ++++++ .../client/oauth2/OAuth2TokenResponse.java | 35 ++++ .../edc/client/oauth2/OkHttpRequestUtils.java | 38 ++++ .../edc/client/oauth2/SovityKeycloakUrl.java | 37 ++++ .../java/de/sovity/edc/client/TestUtils.java | 60 ++++++ extensions/broker-server/build.gradle.kts | 2 +- .../services/api/PolicyDtoBuilder.java | 2 +- .../edc/ext/brokerserver/TestUtils.java | 6 +- .../edc/ext/brokerserver/db/TestDatabase.java | 2 +- .../db/TestDatabaseViaTestcontainers.java | 4 +- .../services/api/CatalogApiTest.java | 172 +++++++++--------- .../services/api/ConnectorApiTest.java | 20 +- .../services/api/DataOfferDetailApiTest.java | 28 +-- .../refreshing/ConnectorUpdaterTest.java | 4 +- .../offers/DataOfferWriterTestDataHelper.java | 10 +- .../DataOfferWriterTestResultHelper.java | 4 +- .../OfflineConnectorRemovalJobTest.java | 2 +- settings.gradle.kts | 1 + 28 files changed, 960 insertions(+), 128 deletions(-) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java create mode 100644 extensions/broker-server-api/client/README.md create mode 100644 extensions/broker-server-api/client/build.gradle.kts create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java create mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java create mode 100644 extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java new file mode 100644 index 000000000..d77ce14ce --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; + +@OpenAPIDefinition( + info = @Info( + title = "Broker Server API", + version = "0.0.0", + description = "Broker Server API for the Broker Server built by sovity.", + contact = @Contact( + name = "sovity GmbH", + email = "contact@sovity.de", + url = "https://github.com/sovity/edc-broker-server-extension/issues/new/choose" + ), + license = @License( + name = "Apache 2.0", + url = "https://github.com/sovity/edc-broker-server-extension/blob/main/LICENSE" + ) + ), + externalDocs = @ExternalDocumentation( + description = "Broker Server API in sovity/edc-broker-server-extension", + url = "https://github.com/sovity/edc-broker-server-extension/tree/main/extensions/broker-server-api" + ) +) +public interface ApiInformation { +} diff --git a/extensions/broker-server-api/client/README.md b/extensions/broker-server-api/client/README.md new file mode 100644 index 000000000..e03892b1e --- /dev/null +++ b/extensions/broker-server-api/client/README.md @@ -0,0 +1,107 @@ + +
+
+ + Logo + + +

EDC-Connector Extension:
API Wrapper & API Clients:
Java API Client

+ +

+ Report Bug + · + Request Feature +

+
+ +## About this component + +Java API Client Library to be imported and used in arbitrary applications like use-case backends. + +An example project using this client can be found [here](../client-example). + +## Installation + +```xml + + + de.sovity.edc + client + ${sovity-edc-extensions.version} + +``` + +## Usage + +### Example Using API Key Auth + +```java +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.brokerserver.client.gen.model.KpiResult; + +/** + * Example using a sovity Community Edition EDC Connector + */ +public class WrapperClientExample { + + public static final String CONNECTOR_ENDPOINT = "http://localhost:11002/api/v1/management"; + public static final String CONNECTOR_API_KEY = "..."; + + public static void main(String[] args) { + // Configure Client + EdcClient client = EdcClient.builder() + .managementApiUrl(CONNECTOR_ENDPOINT) + .managementApiKey(CONNECTOR_API_KEY) + .build(); + + // EDC API Wrapper APIs are now available for use + KpiResult kpiResult = client.useCaseApi().kpiEndpoint(); + System.out.println(kpiResult); + } +} + +``` + +### Example Using OAuth2 Client Credentials + +```java +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.brokerserver.client.gen.model.KpiResult; +import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; +import de.sovity.edc.client.oauth2.SovityKeycloakUrl; + +/** + * Example using a productive Connector-as-a-Service (CaaS) EDC Connector + */ +public class WrapperClientExample { + + public static final String CONNECTOR_ENDPOINT = + "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/data"; + public static final String CLIENT_ID = "{{your-connector}}-app"; + public static final String CLIENT_SECRET = "..."; + + public static void main(String[] args) { + // Configure Client + EdcClient client = EdcClient.builder() + .managementApiUrl(CONNECTOR_ENDPOINT) + .oauth2ClientCredentials(OAuth2ClientCredentials.builder() + .tokenUrl(SovityKeycloakUrl.PRODUCTION) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .build()) + .build(); + + // EDC API Wrapper APIs are now available for use + KpiResult kpiResult = client.useCaseApi().kpiEndpoint(); + System.out.println(kpiResult); + } +} +``` + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts new file mode 100644 index 000000000..39965ce3a --- /dev/null +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -0,0 +1,132 @@ +val edcVersion: String by project +val edcGroup: String by project +val restAssured: String by project +val assertj: String by project + + +plugins { + `java-library` + `maven-publish` + id("org.openapi.generator") version "6.6.0" +} + +repositories { + mavenCentral() +} + +// By using a separate configuration we can skip having the Extension Jar in our runtime classpath +val openapiYaml = configurations.create("openapiGenerator") + +dependencies { + // We only need the openapi.yaml file from this dependency + openapiYaml(project(":extensions:broker-server-api:api")) { + isTransitive = false + } + + // Generated Client's Dependencies + implementation("io.swagger:swagger-annotations:1.6.11") + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("com.google.code.gson:gson:2.10.1") + implementation("io.gsonfire:gson-fire:1.8.5") + implementation("org.openapitools:jackson-databind-nullable:0.2.6") + implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("jakarta.annotation:jakarta.annotation-api:1.3.5") + + // Lombok + compileOnly("org.projectlombok:lombok:1.18.28") + annotationProcessor("org.projectlombok:lombok:1.18.28") + + testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation("${edcGroup}:http:${edcVersion}") + testImplementation(project(":extensions:broker-server-api:api")) + testImplementation("io.rest-assured:rest-assured:${restAssured}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.assertj:assertj-core:${assertj}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +// Extract the openapi file from the JAR +val openapiFile = "broker-server.yaml" +task("extractOpenapiYaml") { + dependsOn(openapiYaml) + into("${project.buildDir}") + from(zipTree(openapiYaml.singleFile)) { + include("broker-server.yaml") + } +} + +tasks.getByName("openApiGenerate") { + dependsOn("extractOpenapiYaml") + generatorName.set("java") + configOptions.set(mutableMapOf( + "invokerPackage" to "de.sovity.edc.ext.brokerserver.client.gen", + "apiPackage" to "de.sovity.edc.ext.brokerserver.client.gen.api", + "modelPackage" to "de.sovity.edc.ext.brokerserver.client.gen.model", + "caseInsensitiveResponseHeaders" to "true", + "additionalModelTypeAnnotations" to "@lombok.AllArgsConstructor\n@lombok.Builder", + "annotationLibrary" to "swagger1", + "hideGenerationTimestamp" to "true", + "useRuntimeException" to "true", + )) + + inputSpec.set("${project.buildDir}/${openapiFile}") + outputDir.set("${project.buildDir}/generated/client-project") +} + +task("postprocessGeneratedClient") { + dependsOn("openApiGenerate") + from("${project.buildDir}/generated/client-project/src/main/java") + + // @lombok.Builder clashes with the following generated model file. + // It is the base class for OAS3 polymorphism via allOf/anyOf, which we won't use anyway. + exclude("**/AbstractOpenApiSchema.java") + + // The Jax-RS dependency suggested by the generated project was causing issues with quarkus. + // It was again only required for the polymorphism, which we won't use anyway. + filter { if (it == "import javax.ws.rs.core.GenericType;") "" else it } + + into("${project.buildDir}/generated/sources/openapi/java/main") +} +sourceSets["main"].java.srcDir("${project.buildDir}/generated/sources/openapi/java/main") + +checkstyle { + // Checkstyle loathes the generated files + // TODO make checkstyle skip generated files only + this.sourceSets = emptyList() +} + + +tasks.getByName("compileJava") { + dependsOn("postprocessGeneratedClient") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + withSourcesJar() + withJavadocJar() +} + +tasks.withType { + val fullOptions = this.options as StandardJavadocDocletOptions + fullOptions.tags = listOf("http.response.details:a:Http Response Details") + fullOptions.addStringOption("Xdoclint:none", "-quiet") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java new file mode 100644 index 000000000..6c9c96f4c --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import de.sovity.edc.ext.brokerserver.client.gen.api.BrokerServerApi; +import lombok.Value; +import lombok.experimental.Accessors; + +/** + * API Client for our EDC API Wrapper. + */ +@Value +@Accessors(fluent = true) +public class EdcClient { + BrokerServerApi brokerServerApi; + + public static EdcClientBuilder builder() { + return new EdcClientBuilder(); + } +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java new file mode 100644 index 000000000..dafc8d53d --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Setter +@Accessors(fluent = true, chain = true) +public class EdcClientBuilder { + /** + * Management API Base URL, e.g. https://my-connector.com/control/management + */ + private String managementApiUrl; + + /** + * Enables EDC Management API Key authentication. + */ + private String managementApiKey = "ApiKeyDefaultValue"; + + /** + * Enables OAuth2 "Client Credentials Flow" authentication. + */ + private OAuth2ClientCredentials oauth2ClientCredentials; + + public EdcClient build() { + return EdcClientFactory.newClient(this); + } +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java new file mode 100644 index 000000000..3128d2030 --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import de.sovity.edc.ext.brokerserver.client.gen.ApiClient; +import de.sovity.edc.ext.brokerserver.client.gen.api.BrokerServerApi; +import de.sovity.edc.client.oauth2.OAuth2CredentialsAuthenticator; +import de.sovity.edc.client.oauth2.OAuth2CredentialsStore; +import de.sovity.edc.client.oauth2.OAuth2CredentialsInterceptor; +import de.sovity.edc.client.oauth2.OAuth2TokenFetcher; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Builds {@link EdcClient}s. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EdcClientFactory { + + public static EdcClient newClient(EdcClientBuilder builder) { + var apiClient = new ApiClient() + .setServerIndex(null) + .setBasePath(builder.managementApiUrl()); + + if (StringUtils.isNotBlank(builder.managementApiKey())) { + apiClient.addDefaultHeader("X-Api-Key", builder.managementApiKey()); + } + + if (builder.oauth2ClientCredentials() != null) { + var tokenFetcher = new OAuth2TokenFetcher(builder.oauth2ClientCredentials()); + var handler = new OAuth2CredentialsStore(tokenFetcher); + var httpClient = apiClient.getHttpClient() + .newBuilder() + .addInterceptor(new OAuth2CredentialsInterceptor(handler)) + .authenticator(new OAuth2CredentialsAuthenticator(handler)) + .build(); + apiClient.setHttpClient(httpClient); + } + + return new EdcClient( + new BrokerServerApi(apiClient) + ); + } +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java new file mode 100644 index 000000000..b202ed6a5 --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class OAuth2ClientCredentials { + @NonNull + private String tokenUrl; + @NonNull + private String clientId; + @NonNull + private String clientSecret; +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java new file mode 100644 index 000000000..6a9d3486a --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.RequiredArgsConstructor; +import okhttp3.Authenticator; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Route; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * OkHttp Authenticator: Potentially re-tries requests that failed with a 401 / 403 + * with updated access tokens. + */ +@RequiredArgsConstructor +public class OAuth2CredentialsAuthenticator implements Authenticator { + private final OAuth2CredentialsStore credentialsStore; + + @Nullable + @Override + public Request authenticate(@Nullable Route route, @NotNull Response response) { + // Skip if original request had no authentication + if (!OkHttpRequestUtils.hadBearerToken(response)) { + return null; + } + + var token = credentialsStore.getAccessToken(); + synchronized (this) { + // The synchronized Block prevents multiple parallel token refreshes + // So here the token might have changed already + var changedToken = credentialsStore.getAccessToken(); + + // If the token has changed since the request was made, use the new token. + if (!changedToken.equals(token)) { + return OkHttpRequestUtils.withBearerToken(response.request(), changedToken); + } + + // If the token hasn't changed, try to be the code path to refresh the token + var updatedToken = credentialsStore.refreshAccessToken(); + + // Retry the request with the new token. + return OkHttpRequestUtils.withBearerToken(response.request(), updatedToken); + } + } +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java new file mode 100644 index 000000000..dd52d2d8d --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.RequiredArgsConstructor; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * OkHttp Interceptor: Adds Bearer Token to requests + */ +@RequiredArgsConstructor +public class OAuth2CredentialsInterceptor implements Interceptor { + private final OAuth2CredentialsStore credentialsStore; + + @NotNull + @Override + public Response intercept(Chain chain) throws IOException { + String accessToken = credentialsStore.getAccessToken(); + Request request = OkHttpRequestUtils.withBearerToken(chain.request(), accessToken); + return chain.proceed(request); + } + +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java new file mode 100644 index 000000000..0b02da89d --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.SneakyThrows; + +/** + * Holds the Access Token and coordinates it between the Interceptor and the Authenticator. + */ +public class OAuth2CredentialsStore { + private final OAuth2TokenFetcher tokenFetcher; + private OAuth2TokenResponse tokenResponse = null; + + public OAuth2CredentialsStore(OAuth2TokenFetcher tokenFetcher) { + this.tokenFetcher = tokenFetcher; + this.fetchAccessTokenInternal(); + } + + public String getAccessToken() { + synchronized (this) { + if (tokenResponse == null) { + fetchAccessTokenInternal(); + } + return tokenResponse.getAccessToken(); + } + } + + public String refreshAccessToken() { + synchronized (this) { + fetchAccessTokenInternal(); + return tokenResponse.getAccessToken(); + } + } + + @SneakyThrows + private void fetchAccessTokenInternal() { + // If it crashes afterwards, the next request won't attempt to use the old token + tokenResponse = null; + tokenResponse = tokenFetcher.fetchToken(); + } + +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java new file mode 100644 index 000000000..009d03768 --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import de.sovity.edc.ext.brokerserver.client.gen.ApiClient; +import de.sovity.edc.ext.brokerserver.client.gen.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import okhttp3.Call; +import okhttp3.FormBody; +import okhttp3.Request; + +/** + * OAuth2 Token Response Fetcher for the "Client Credentials Grant" Flow + */ +@RequiredArgsConstructor +public class OAuth2TokenFetcher { + private final OAuth2ClientCredentials clientCredentials; + private final ApiClient apiClient = new ApiClient(); + + /** + * Fetch an access token for a "Client Credentials" Grant + * + * @return the token response including the access token + */ + @SneakyThrows + public OAuth2TokenResponse fetchToken() { + var formData = new FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", clientCredentials.getClientId()) + .add("client_secret", clientCredentials.getClientSecret()) + .build(); + + var request = new Request.Builder() + .url(clientCredentials.getTokenUrl()) + .post(formData) + .build(); + + // Re-use the Utils for OkHttp from the OpenAPI generator + Call call = apiClient.getHttpClient().newCall(request); + ApiResponse response = apiClient.execute(call, OAuth2TokenResponse.class); + return response.getData(); + } +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java new file mode 100644 index 000000000..5096169b9 --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import com.google.gson.annotations.SerializedName; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class OAuth2TokenResponse { + + @SerializedName("access_token") + private String accessToken; +} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java new file mode 100644 index 000000000..10ee1ca4e --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import okhttp3.Request; +import okhttp3.Response; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class OkHttpRequestUtils { + public static boolean hadBearerToken(@NonNull Response response) { + String header = response.request().header("Authorization"); + return header != null && header.startsWith("Bearer"); + } + + @NonNull + public static Request withBearerToken(@NonNull Request request, @NonNull String accessToken) { + return request.newBuilder() + .removeHeader("Authorization") + .header("Authorization", "Bearer " + accessToken) + .build(); + } +} + diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java new file mode 100644 index 000000000..1175628e6 --- /dev/null +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * Quick access to the Keycloak OAuth Token URLs for our staging and production environments. + *

+ * For ease of use of our API Wrapper Client Libraries in Use Case Applications. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SovityKeycloakUrl { + + /** + * Sovity Production Keycloak OAuth2 Token URL + */ + public static final String PRODUCTION = "https://keycloak.prod-sovity.azure.sovity.io/realms/Portal/protocol/openid-connect/token"; + + /** + * Sovity Staging Keycloak OAuth2 Token URL + */ + public static final String STAGING = "https://keycloak.stage-sovity.azure.sovity.io/realms/Portal/protocol/openid-connect/token"; +} diff --git a/extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java b/extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java new file mode 100644 index 000000000..8bc0e48f6 --- /dev/null +++ b/extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; + +public class TestUtils { + private static final int DATA_PORT = getFreePort(); + private static final int PROTOCOL_PORT = getFreePort(); + private static final String DATA_PATH = "/api/v1/data"; + private static final String PROTOCOL_PATH = "/api/v1/ids"; + public static final String MANAGEMENT_API_KEY = "123456"; + public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + DATA_PATH; + + + public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; + public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH + "/data"; + + @NotNull + public static Map createConfiguration( + Map additionalConfigProperties + ) { + Map config = new HashMap<>(); + config.put("web.http.port", String.valueOf(getFreePort())); + config.put("web.http.path", "/api"); + config.put("web.http.management.port", String.valueOf(DATA_PORT)); + config.put("web.http.management.path", DATA_PATH); + config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); + config.put("web.http.protocol.path", PROTOCOL_PATH); + config.put("edc.api.auth.key", MANAGEMENT_API_KEY); + config.put("edc.ids.endpoint", PROTOCOL_ENDPOINT); + config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); + config.putAll(additionalConfigProperties); + return config; + } + + public static EdcClient edcClient() { + return EdcClient.builder() + .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) + .managementApiKey(TestUtils.MANAGEMENT_API_KEY) + .build(); + } +} diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 427630cc4..1b6994c48 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -42,7 +42,7 @@ dependencies { testImplementation("${edcGroup}:ids:${edcVersion}") testImplementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") testImplementation("${edcGroup}:configuration-filesystem:${edcVersion}") - testImplementation("${sovityEdcGroup}:client:${sovityEdcExtensionsVersion}") + testImplementation(project(":extensions:broker-server-api:client")) testImplementation("io.rest-assured:rest-assured:${restAssured}") testImplementation("org.testcontainers:testcontainers:${testcontainersVersion}") testImplementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java index baffa368a..f5961e73e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -24,6 +24,6 @@ public class PolicyDtoBuilder { @SneakyThrows public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { - return new PolicyDto(policyJson, null); + return new PolicyDto(policyJson); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 6f6e21d79..fc1bac49f 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -85,8 +85,8 @@ private static Map getCoreEdcJdbcConfig(TestDatabase testDatabas public static EdcClient edcClient() { return EdcClient.builder() - .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) - .managementApiKey(TestUtils.MANAGEMENT_API_KEY) - .build(); + .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) + .managementApiKey(TestUtils.MANAGEMENT_API_KEY) + .build(); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index 9e074fbd1..20e690f4b 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -19,8 +19,8 @@ import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; -import java.util.function.Consumer; import javax.sql.DataSource; +import java.util.function.Consumer; public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { String getJdbcUrl(); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java index b43fb3aa6..73134808f 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java @@ -19,8 +19,8 @@ public class TestDatabaseViaTestcontainers implements TestDatabase { private PostgreSQLContainer container = new PostgreSQLContainer<>("postgres:15-alpine") - .withUsername("edc") - .withPassword("edc"); + .withUsername("edc") + .withPassword("edc"); @Override public void afterAll(ExtensionContext context) throws Exception { diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 38a55e2f6..0e4ebe5a2 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -15,14 +15,14 @@ package de.sovity.edc.ext.brokerserver.services.api; import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.client.gen.model.CatalogDataOffer; -import de.sovity.edc.client.gen.model.CatalogPageQuery; -import de.sovity.edc.client.gen.model.CatalogPageResult; -import de.sovity.edc.client.gen.model.CnfFilterAttribute; -import de.sovity.edc.client.gen.model.CnfFilterItem; -import de.sovity.edc.client.gen.model.CnfFilterValue; -import de.sovity.edc.client.gen.model.CnfFilterValueAttribute; import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogDataOffer; +import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; +import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageResult; +import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterAttribute; +import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterItem; +import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValue; +import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValueAttribute; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; @@ -61,9 +61,9 @@ class CatalogApiTest { @BeforeEach void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", - BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" + BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", + BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", + BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" ))); } @@ -76,17 +76,17 @@ void testDataSpace_two_dataspaces_filter_for_one() { createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: MDS createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector2/ids/data"); // Dataspace: Example1 var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataSpace", List.of("Example1")) + new CnfFilterValueAttribute("dataSpace", List.of("Example1")) ))); var result = edcClient().brokerServerApi().catalogPage(query); @@ -107,24 +107,24 @@ void test_available_filter_values_to_filter_by() { createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 createConnector(dsl, today, "http://my-connector3/ids/data"); // Dataspace: Example2 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "de" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "de" ), "http://my-connector/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "en" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "en" ), "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset2", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" + AssetProperty.ASSET_ID, "urn:artifact:my-asset2", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" ), "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset3", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" + AssetProperty.ASSET_ID, "urn:artifact:my-asset3", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" ), "http://my-connector3/ids/data"); // Dataspace: Example2 // get all available filter values @@ -133,9 +133,9 @@ void test_available_filter_values_to_filter_by() { // assert that the filter values are correct var dataSpace = getAvailableFilter(result, "dataSpace"); assertThat(dataSpace.getValues()).containsExactly( - new CnfFilterItem("Example1", "Example1"), - new CnfFilterItem("Example2", "Example2"), - new CnfFilterItem("MDS", "MDS") + new CnfFilterItem("Example1", "Example1"), + new CnfFilterItem("Example2", "Example2"), + new CnfFilterItem("MDS", "MDS") ); }); } @@ -148,8 +148,8 @@ void testDataOfferDetails() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector/ids/data"); @@ -162,15 +162,15 @@ void testDataOfferDetails() { assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(CatalogDataOffer.ConnectorOnlineStatusEnum.ONLINE); assertThat(dataOfferResult.getAssetId()).isEqualTo("urn:artifact:my-asset"); assertThat(dataOfferResult.getProperties()).isEqualTo(Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" )); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); // Key order of Json-String might differ, so we compare the JSON-Objects for similarity - var actual = dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy(); - var expected = toJson(dummyPolicy()); - assertEqualJson(expected, actual); +// var actual = dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy(); +// var expected = toJson(dummyPolicy()); +// assertEqualJson(expected, toJson(actual)); }); } @@ -204,53 +204,53 @@ void testAvailableFilters_noFilter() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", - AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", + AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "" ), "http://my-connector/ids/data"); var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getId) - .containsExactly( - "dataSpace", - AssetProperty.DATA_CATEGORY, - AssetProperty.DATA_SUBCATEGORY, - AssetProperty.DATA_MODEL, - AssetProperty.TRANSPORT_MODE, - AssetProperty.GEO_REFERENCE_METHOD - ); + .extracting(CnfFilterAttribute::getId) + .containsExactly( + "dataSpace", + AssetProperty.DATA_CATEGORY, + AssetProperty.DATA_SUBCATEGORY, + AssetProperty.DATA_MODEL, + AssetProperty.TRANSPORT_MODE, + AssetProperty.GEO_REFERENCE_METHOD + ); assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getTitle) - .containsExactly( - "Data Space", - "Data Category", - "Data Subcategory", - "Data Model", - "Transport Mode", - "Geo Reference Method" - ); + .extracting(CnfFilterAttribute::getTitle) + .containsExactly( + "Data Space", + "Data Category", + "Data Subcategory", + "Data Model", + "Transport Mode", + "Geo Reference Method" + ); var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); assertThat(dataCategory.getTitle()).isEqualTo("Data Category"); @@ -271,8 +271,8 @@ void testAvailableFilters_noFilter() { private CnfFilterAttribute getAvailableFilter(CatalogPageResult result, String filterId) { return result.getAvailableFilters().getFields().stream() - .filter(it -> it.getId().equals(filterId)).findFirst() - .orElseThrow(() -> new IllegalStateException("Filter not found")); + .filter(it -> it.getId().equals(filterId)).findFirst() + .orElseThrow(() -> new IllegalStateException("Filter not found")); } @Test @@ -283,19 +283,19 @@ void testAvailableFilters_withFilter() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.DATA_SUBCATEGORY, "my-subcategory" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.DATA_SUBCATEGORY, "my-subcategory" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_SUBCATEGORY, "my-other-subcategory" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_SUBCATEGORY, "my-other-subcategory" ), "http://my-connector/ids/data"); var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) + new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) ))); var result = edcClient().brokerServerApi().catalogPage(query); @@ -322,10 +322,10 @@ void testPagination_firstPage() { createConnector(dsl, today, "http://my-connector/ids/data"); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) ), "http://my-connector/ids/data")); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) + AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) ), "http://my-connector/ids/data")); @@ -335,7 +335,7 @@ void testPagination_firstPage() { var result = edcClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(IntStream.range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + .isEqualTo(IntStream.range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(1); @@ -353,10 +353,10 @@ void testPagination_secondPage() { createConnector(dsl, today, "http://my-connector/ids/data"); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) ), "http://my-connector/ids/data")); IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) + AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) ), "http://my-connector/ids/data")); @@ -368,7 +368,7 @@ void testPagination_secondPage() { var result = edcClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(IntStream.range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + .isEqualTo(IntStream.range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(2); @@ -413,8 +413,8 @@ private void createConnector(DSLContext dsl, OffsetDateTime today, String connec private Policy dummyPolicy() { return Policy.Builder.newInstance() - .assignee("Example Assignee") - .build(); + .assignee("Example Assignee") + .build(); } private String policyToJson(Policy policy) { diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index 6e49c727a..44e414268 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -15,8 +15,8 @@ package de.sovity.edc.ext.brokerserver.services.api; import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.client.gen.model.ConnectorDetailPageQuery; -import de.sovity.edc.client.gen.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; @@ -63,9 +63,9 @@ void testQueryConnectors() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" ), "http://my-connector/ids/data"); var result = edcClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); @@ -87,9 +87,9 @@ void testQueryConnectorDetails() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" ), "http://my-connector/ids/data"); var connector = edcClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("http://my-connector/ids/data")); @@ -136,8 +136,8 @@ private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map dummyContractOffer(dataOffer, contractOffer)) - .forEach(existingContractOffers::add); + .map(contractOffer -> dummyContractOffer(dataOffer, contractOffer)) + .forEach(existingContractOffers::add); } public void initialize(DSLContext dsl) { @@ -113,14 +113,14 @@ private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { public String dummyAssetJson(Do dataOffer) { return "{\"%s\": \"%s\", \"%s\": \"%s\"}".formatted( - AssetProperty.ASSET_ID, dataOffer.getAssetId(), - AssetProperty.ASSET_NAME, dataOffer.getAssetName() + AssetProperty.ASSET_ID, dataOffer.getAssetId(), + AssetProperty.ASSET_NAME, dataOffer.getAssetName() ); } public String dummyPolicyJson(String policyValue) { return "{\"%s\": \"%s\"}".formatted( - "SomePolicyField", policyValue + "SomePolicyField", policyValue ); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java index acdefc766..db42a2e3c 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java @@ -33,8 +33,8 @@ class DataOfferWriterTestResultHelper { DataOfferWriterTestResultHelper(DSLContext dsl) { this.dataOffers = dsl.selectFrom(Tables.DATA_OFFER).fetchMap(Tables.DATA_OFFER.ASSET_ID); this.contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().collect(groupingBy( - DataOfferContractOfferRecord::getAssetId, - Collectors.toMap(DataOfferContractOfferRecord::getContractOfferId, Function.identity()) + DataOfferContractOfferRecord::getAssetId, + Collectors.toMap(DataOfferContractOfferRecord::getContractOfferId, Function.identity()) )); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java index f3278a43a..018d674da 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java @@ -56,7 +56,7 @@ static void beforeAll() { void beforeEach() { brokerServerSettings = mock(BrokerServerSettings.class); offlineConnectorRemover = new OfflineConnectorRemover( - brokerServerSettings, + brokerServerSettings, new ConnectorQueries(), new BrokerEventLogger() ); diff --git a/settings.gradle.kts b/settings.gradle.kts index 4dfb2a8ec..bf9727aed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,5 @@ rootProject.name = "edc-broker-server-extension" include(":extensions:broker-server") include(":extensions:broker-server-postgres-flyway-jooq") include(":extensions:broker-server-api:api") +include(":extensions:broker-server-api:client") include(":connector") From 18a9c73d061b62f4a35db53040ae9b1889fb0173 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 30 Jun 2023 11:17:38 +0200 Subject: [PATCH 080/295] chore: finalize broker server api client, improved READMEs, fixed ts client pipeline (#170) --- .../build-and-publish-ts-api-client.yml | 2 +- CHANGELOG.md | 15 +++- .../broker-server-api/{ => api}/README.md | 6 +- .../broker-server-api/client-ts/README.md | 55 +++++++++++++ .../client-ts/src/BrokerServerClient.ts | 6 +- extensions/broker-server-api/client/README.md | 78 +++++-------------- .../broker-server-api/client/build.gradle.kts | 13 +--- .../oauth2/OAuth2ClientCredentials.java | 36 --------- .../OAuth2CredentialsAuthenticator.java | 59 -------------- .../oauth2/OAuth2CredentialsInterceptor.java | 40 ---------- .../client/oauth2/OAuth2CredentialsStore.java | 54 ------------- .../edc/client/oauth2/OAuth2TokenFetcher.java | 56 ------------- .../client/oauth2/OAuth2TokenResponse.java | 35 --------- .../edc/client/oauth2/OkHttpRequestUtils.java | 38 --------- .../edc/client/oauth2/SovityKeycloakUrl.java | 37 --------- .../client/BrokerServerClient.java} | 10 +-- .../client/BrokerServerClientBuilder.java} | 16 ++-- .../client/BrokerServerClientFactory.java} | 25 ++---- .../java/de/sovity/edc/client/TestUtils.java | 60 -------------- .../edc/ext/brokerserver/TestUtils.java | 6 +- 20 files changed, 115 insertions(+), 532 deletions(-) rename extensions/broker-server-api/{ => api}/README.md (75%) create mode 100644 extensions/broker-server-api/client-ts/README.md delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java rename extensions/broker-server-api/client/src/main/java/de/sovity/edc/{client/EdcClient.java => ext/brokerserver/client/BrokerServerClient.java} (72%) rename extensions/broker-server-api/client/src/main/java/de/sovity/edc/{client/EdcClientBuilder.java => ext/brokerserver/client/BrokerServerClientBuilder.java} (61%) rename extensions/broker-server-api/client/src/main/java/de/sovity/edc/{client/EdcClientFactory.java => ext/brokerserver/client/BrokerServerClientFactory.java} (50%) delete mode 100644 extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java diff --git a/.github/workflows/build-and-publish-ts-api-client.yml b/.github/workflows/build-and-publish-ts-api-client.yml index 9e6d16e31..128fbaad6 100644 --- a/.github/workflows/build-and-publish-ts-api-client.yml +++ b/.github/workflows/build-and-publish-ts-api-client.yml @@ -62,4 +62,4 @@ jobs: npm publish --access public --tag "${{ env.DIST_TAG }}" env: NODE_USER: richardtreier-sovity - NODE_AUTH_TOKEN: ${{ secrets.SOVITY_EDC_CLIENT_NPM_AUTH }} + NODE_AUTH_TOKEN: ${{ secrets.SOVITY_BROKER_SERVER_CLIENT_NPM_AUTH }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cb051e7..25f690671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - yyyy-mm-dd -### Deployment Migration Notes +### Overview -### Major +### Detailed Changes + +#### Major -### Minor +- Broker Server API now generates into it's own Broker Server Client Typescript Library. -### Patch +#### Minor + +- Broker Server API is now part of this repository. +- Dead Connectors are now deleted periodically. + +#### Patch ### Deployment Migration Notes diff --git a/extensions/broker-server-api/README.md b/extensions/broker-server-api/api/README.md similarity index 75% rename from extensions/broker-server-api/README.md rename to extensions/broker-server-api/api/README.md index b139b827d..22cb75f65 100644 --- a/extensions/broker-server-api/README.md +++ b/extensions/broker-server-api/api/README.md @@ -5,7 +5,7 @@ Logo -

Broker Server / Broker UI API Specification

+

Broker Server API Specification

Report Bug @@ -16,11 +16,11 @@ ## About this component -Specification of Broker Server API endpoints as required for the Broker UI. +Specification of Broker Server API endpoints, for example endpoints for the Broker UI. ## License -Apache License 2.0 - see [LICENSE](../../LICENSE.md) +Apache License 2.0 - see [LICENSE](../../../LICENSE.md) ## Contact diff --git a/extensions/broker-server-api/client-ts/README.md b/extensions/broker-server-api/client-ts/README.md new file mode 100644 index 000000000..ad2997a92 --- /dev/null +++ b/extensions/broker-server-api/client-ts/README.md @@ -0,0 +1,55 @@ + +
+

+ + Logo + + +

Broker Server API TypeScript Client Library

+ +

+ Report Bug + · + Request Feature +

+
+ +## About this component + +TypeScript Client Library to access APIs of our Broker Server Backend. + +## How to install + +Requires a NodeJS / NPM project. + +```shell script +npm i --save @sovity.de/broker-server-client +``` + +## How to use + +Configure your Broker Server Client and use endpoints of our Broker Server API: + +```typescript +import { + BrokerServerClient, + buildBrokerServerClient, + CatalogPageResult +} from '@sovity.de/broker-server-client'; + +const brokerServerClient: BrokerServerClient = buildBrokerServerClient({ + managementApiUrl: 'http://localhost:11002/api/v1/management', + managementApiKey: 'ApiKeyDefaultValue', +}); + +let catalog: CatalogPageResult = await edcClient.brokerServerApi.catalogPage(); +``` + +## License + +Apache License 2.0 - see +[LICENSE](https://github.com/sovity/edc-broker-server-extension/blob/main/LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts b/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts index 2dd606cc1..f03bee56b 100644 --- a/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts +++ b/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts @@ -1,7 +1,7 @@ import { BrokerServerApi, Configuration, - ConfigurationParameters + ConfigurationParameters, } from './generated'; /** @@ -15,7 +15,9 @@ export interface BrokerServerClient { * Configure & Build new Broker Server Client * @param opts opts */ -export function buildBrokerServerClient(opts: BrokerServerClientOptions): BrokerServerClient { +export function buildBrokerServerClient( + opts: BrokerServerClientOptions, +): BrokerServerClient { const config = new Configuration({ basePath: opts.managementApiUrl, headers: { diff --git a/extensions/broker-server-api/client/README.md b/extensions/broker-server-api/client/README.md index e03892b1e..69b6b616a 100644 --- a/extensions/broker-server-api/client/README.md +++ b/extensions/broker-server-api/client/README.md @@ -1,19 +1,20 @@
- + Logo -

EDC-Connector Extension:
API Wrapper & API Clients:
Java API Client

+

Broker Server API Java Client Library

- Report Bug + Report Bug · - Request Feature + Request Feature

+ ## About this component Java API Client Library to be imported and used in arbitrary applications like use-case backends. @@ -25,75 +26,38 @@ An example project using this client can be found [here](../client-example). ```xml - de.sovity.edc + de.sovity.broker client - ${sovity-edc-extensions.version} + ${sovity-edc-broker-server-extension.version} ``` -## Usage - -### Example Using API Key Auth - -```java -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.ext.brokerserver.client.gen.model.KpiResult; - -/** - * Example using a sovity Community Edition EDC Connector - */ -public class WrapperClientExample { - - public static final String CONNECTOR_ENDPOINT = "http://localhost:11002/api/v1/management"; - public static final String CONNECTOR_API_KEY = "..."; - - public static void main(String[] args) { - // Configure Client - EdcClient client = EdcClient.builder() - .managementApiUrl(CONNECTOR_ENDPOINT) - .managementApiKey(CONNECTOR_API_KEY) - .build(); - - // EDC API Wrapper APIs are now available for use - KpiResult kpiResult = client.useCaseApi().kpiEndpoint(); - System.out.println(kpiResult); - } -} - -``` - -### Example Using OAuth2 Client Credentials +## Usage Example ```java -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.ext.brokerserver.client.gen.model.KpiResult; -import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; -import de.sovity.edc.client.oauth2.SovityKeycloakUrl; +import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; +import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; +import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageResult; /** - * Example using a productive Connector-as-a-Service (CaaS) EDC Connector + * Example using the Broker Server API Java Client Library */ -public class WrapperClientExample { +public class BrokerServerClientExample { - public static final String CONNECTOR_ENDPOINT = - "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/data"; - public static final String CLIENT_ID = "{{your-connector}}-app"; - public static final String CLIENT_SECRET = "..."; + public static final String BROKER_MANAGEMENT_API_URL = "http://localhost:11002/api/v1/management"; + public static final String BROKER_MANAGEMENT_API_KEY = "..."; public static void main(String[] args) { // Configure Client - EdcClient client = EdcClient.builder() - .managementApiUrl(CONNECTOR_ENDPOINT) - .oauth2ClientCredentials(OAuth2ClientCredentials.builder() - .tokenUrl(SovityKeycloakUrl.PRODUCTION) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .build()) + BrokerServerClient client = BrokerServerClient.builder() + .managementApiUrl(BROKER_MANAGEMENT_API_URL) + .managementApiKey(BROKER_MANAGEMENT_API_KEY) .build(); // EDC API Wrapper APIs are now available for use - KpiResult kpiResult = client.useCaseApi().kpiEndpoint(); - System.out.println(kpiResult); + CatalogPageQuery catalogPageQuery = new CatalogPageQuery(); + CatalogPageResult catalogPageResult = client.brokerServerApi().catalogPage(catalogPageQuery); + System.out.println(catalogPageResult.getDataOffers()); } } ``` diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index 39965ce3a..a78dfb2f9 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -37,15 +37,6 @@ dependencies { // Lombok compileOnly("org.projectlombok:lombok:1.18.28") annotationProcessor("org.projectlombok:lombok:1.18.28") - - testImplementation("${edcGroup}:control-plane-core:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") - testImplementation("${edcGroup}:http:${edcVersion}") - testImplementation(project(":extensions:broker-server-api:api")) - testImplementation("io.rest-assured:rest-assured:${restAssured}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") - testImplementation("org.assertj:assertj-core:${assertj}") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") } tasks.getByName("test") { @@ -120,8 +111,8 @@ tasks.withType { fullOptions.addStringOption("Xdoclint:none", "-quiet") } -val sovityEdcGroup: String by project -group = sovityEdcGroup +val sovityBrokerServerGroup: String by project +group = sovityBrokerServerGroup publishing { publications { diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java deleted file mode 100644 index b202ed6a5..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; - -/** - * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. - */ -@Getter -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@Builder -public class OAuth2ClientCredentials { - @NonNull - private String tokenUrl; - @NonNull - private String clientId; - @NonNull - private String clientSecret; -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java deleted file mode 100644 index 6a9d3486a..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import lombok.RequiredArgsConstructor; -import okhttp3.Authenticator; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.Route; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * OkHttp Authenticator: Potentially re-tries requests that failed with a 401 / 403 - * with updated access tokens. - */ -@RequiredArgsConstructor -public class OAuth2CredentialsAuthenticator implements Authenticator { - private final OAuth2CredentialsStore credentialsStore; - - @Nullable - @Override - public Request authenticate(@Nullable Route route, @NotNull Response response) { - // Skip if original request had no authentication - if (!OkHttpRequestUtils.hadBearerToken(response)) { - return null; - } - - var token = credentialsStore.getAccessToken(); - synchronized (this) { - // The synchronized Block prevents multiple parallel token refreshes - // So here the token might have changed already - var changedToken = credentialsStore.getAccessToken(); - - // If the token has changed since the request was made, use the new token. - if (!changedToken.equals(token)) { - return OkHttpRequestUtils.withBearerToken(response.request(), changedToken); - } - - // If the token hasn't changed, try to be the code path to refresh the token - var updatedToken = credentialsStore.refreshAccessToken(); - - // Retry the request with the new token. - return OkHttpRequestUtils.withBearerToken(response.request(), updatedToken); - } - } -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java deleted file mode 100644 index dd52d2d8d..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import lombok.RequiredArgsConstructor; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; - -/** - * OkHttp Interceptor: Adds Bearer Token to requests - */ -@RequiredArgsConstructor -public class OAuth2CredentialsInterceptor implements Interceptor { - private final OAuth2CredentialsStore credentialsStore; - - @NotNull - @Override - public Response intercept(Chain chain) throws IOException { - String accessToken = credentialsStore.getAccessToken(); - Request request = OkHttpRequestUtils.withBearerToken(chain.request(), accessToken); - return chain.proceed(request); - } - -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java deleted file mode 100644 index 0b02da89d..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import lombok.SneakyThrows; - -/** - * Holds the Access Token and coordinates it between the Interceptor and the Authenticator. - */ -public class OAuth2CredentialsStore { - private final OAuth2TokenFetcher tokenFetcher; - private OAuth2TokenResponse tokenResponse = null; - - public OAuth2CredentialsStore(OAuth2TokenFetcher tokenFetcher) { - this.tokenFetcher = tokenFetcher; - this.fetchAccessTokenInternal(); - } - - public String getAccessToken() { - synchronized (this) { - if (tokenResponse == null) { - fetchAccessTokenInternal(); - } - return tokenResponse.getAccessToken(); - } - } - - public String refreshAccessToken() { - synchronized (this) { - fetchAccessTokenInternal(); - return tokenResponse.getAccessToken(); - } - } - - @SneakyThrows - private void fetchAccessTokenInternal() { - // If it crashes afterwards, the next request won't attempt to use the old token - tokenResponse = null; - tokenResponse = tokenFetcher.fetchToken(); - } - -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java deleted file mode 100644 index 009d03768..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import de.sovity.edc.ext.brokerserver.client.gen.ApiClient; -import de.sovity.edc.ext.brokerserver.client.gen.ApiResponse; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import okhttp3.Call; -import okhttp3.FormBody; -import okhttp3.Request; - -/** - * OAuth2 Token Response Fetcher for the "Client Credentials Grant" Flow - */ -@RequiredArgsConstructor -public class OAuth2TokenFetcher { - private final OAuth2ClientCredentials clientCredentials; - private final ApiClient apiClient = new ApiClient(); - - /** - * Fetch an access token for a "Client Credentials" Grant - * - * @return the token response including the access token - */ - @SneakyThrows - public OAuth2TokenResponse fetchToken() { - var formData = new FormBody.Builder() - .add("grant_type", "client_credentials") - .add("client_id", clientCredentials.getClientId()) - .add("client_secret", clientCredentials.getClientSecret()) - .build(); - - var request = new Request.Builder() - .url(clientCredentials.getTokenUrl()) - .post(formData) - .build(); - - // Re-use the Utils for OkHttp from the OpenAPI generator - Call call = apiClient.getHttpClient().newCall(request); - ApiResponse response = apiClient.execute(call, OAuth2TokenResponse.class); - return response.getData(); - } -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java deleted file mode 100644 index 5096169b9..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import com.google.gson.annotations.SerializedName; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -/** - * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. - */ -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class OAuth2TokenResponse { - - @SerializedName("access_token") - private String accessToken; -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java deleted file mode 100644 index 10ee1ca4e..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import okhttp3.Request; -import okhttp3.Response; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class OkHttpRequestUtils { - public static boolean hadBearerToken(@NonNull Response response) { - String header = response.request().header("Authorization"); - return header != null && header.startsWith("Bearer"); - } - - @NonNull - public static Request withBearerToken(@NonNull Request request, @NonNull String accessToken) { - return request.newBuilder() - .removeHeader("Authorization") - .header("Authorization", "Bearer " + accessToken) - .build(); - } -} - diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java deleted file mode 100644 index 1175628e6..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client.oauth2; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -/** - * Quick access to the Keycloak OAuth Token URLs for our staging and production environments. - *

- * For ease of use of our API Wrapper Client Libraries in Use Case Applications. - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class SovityKeycloakUrl { - - /** - * Sovity Production Keycloak OAuth2 Token URL - */ - public static final String PRODUCTION = "https://keycloak.prod-sovity.azure.sovity.io/realms/Portal/protocol/openid-connect/token"; - - /** - * Sovity Staging Keycloak OAuth2 Token URL - */ - public static final String STAGING = "https://keycloak.stage-sovity.azure.sovity.io/realms/Portal/protocol/openid-connect/token"; -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java similarity index 72% rename from extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java rename to extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java index 6c9c96f4c..fcfec3d17 100644 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClient.java +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java @@ -12,21 +12,21 @@ * */ -package de.sovity.edc.client; +package de.sovity.edc.ext.brokerserver.client; import de.sovity.edc.ext.brokerserver.client.gen.api.BrokerServerApi; import lombok.Value; import lombok.experimental.Accessors; /** - * API Client for our EDC API Wrapper. + * API Client for the Broker Server. */ @Value @Accessors(fluent = true) -public class EdcClient { +public class BrokerServerClient { BrokerServerApi brokerServerApi; - public static EdcClientBuilder builder() { - return new EdcClientBuilder(); + public static BrokerServerClientBuilder builder() { + return new BrokerServerClientBuilder(); } } diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java similarity index 61% rename from extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java rename to extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java index dafc8d53d..ce5d34e7b 100644 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java @@ -12,9 +12,8 @@ * */ -package de.sovity.edc.client; +package de.sovity.edc.ext.brokerserver.client; -import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @@ -22,9 +21,9 @@ @Getter @Setter @Accessors(fluent = true, chain = true) -public class EdcClientBuilder { +public class BrokerServerClientBuilder { /** - * Management API Base URL, e.g. https://my-connector.com/control/management + * Management API Base URL, e.g. https://my-broker.com/backend/management */ private String managementApiUrl; @@ -33,12 +32,7 @@ public class EdcClientBuilder { */ private String managementApiKey = "ApiKeyDefaultValue"; - /** - * Enables OAuth2 "Client Credentials Flow" authentication. - */ - private OAuth2ClientCredentials oauth2ClientCredentials; - - public EdcClient build() { - return EdcClientFactory.newClient(this); + public BrokerServerClient build() { + return BrokerServerClientFactory.newClient(this); } } diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java similarity index 50% rename from extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java rename to extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java index 3128d2030..57303717c 100644 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/client/EdcClientFactory.java +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java @@ -12,25 +12,21 @@ * */ -package de.sovity.edc.client; +package de.sovity.edc.ext.brokerserver.client; import de.sovity.edc.ext.brokerserver.client.gen.ApiClient; import de.sovity.edc.ext.brokerserver.client.gen.api.BrokerServerApi; -import de.sovity.edc.client.oauth2.OAuth2CredentialsAuthenticator; -import de.sovity.edc.client.oauth2.OAuth2CredentialsStore; -import de.sovity.edc.client.oauth2.OAuth2CredentialsInterceptor; -import de.sovity.edc.client.oauth2.OAuth2TokenFetcher; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; /** - * Builds {@link EdcClient}s. + * Builds {@link BrokerServerClient}s. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) -public class EdcClientFactory { +public class BrokerServerClientFactory { - public static EdcClient newClient(EdcClientBuilder builder) { + public static BrokerServerClient newClient(BrokerServerClientBuilder builder) { var apiClient = new ApiClient() .setServerIndex(null) .setBasePath(builder.managementApiUrl()); @@ -39,18 +35,7 @@ public static EdcClient newClient(EdcClientBuilder builder) { apiClient.addDefaultHeader("X-Api-Key", builder.managementApiKey()); } - if (builder.oauth2ClientCredentials() != null) { - var tokenFetcher = new OAuth2TokenFetcher(builder.oauth2ClientCredentials()); - var handler = new OAuth2CredentialsStore(tokenFetcher); - var httpClient = apiClient.getHttpClient() - .newBuilder() - .addInterceptor(new OAuth2CredentialsInterceptor(handler)) - .authenticator(new OAuth2CredentialsAuthenticator(handler)) - .build(); - apiClient.setHttpClient(httpClient); - } - - return new EdcClient( + return new BrokerServerClient( new BrokerServerApi(apiClient) ); } diff --git a/extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java b/extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java deleted file mode 100644 index 8bc0e48f6..000000000 --- a/extensions/broker-server-api/client/src/test/java/de/sovity/edc/client/TestUtils.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.client; - -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; - -import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; - -public class TestUtils { - private static final int DATA_PORT = getFreePort(); - private static final int PROTOCOL_PORT = getFreePort(); - private static final String DATA_PATH = "/api/v1/data"; - private static final String PROTOCOL_PATH = "/api/v1/ids"; - public static final String MANAGEMENT_API_KEY = "123456"; - public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + DATA_PATH; - - - public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; - public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH + "/data"; - - @NotNull - public static Map createConfiguration( - Map additionalConfigProperties - ) { - Map config = new HashMap<>(); - config.put("web.http.port", String.valueOf(getFreePort())); - config.put("web.http.path", "/api"); - config.put("web.http.management.port", String.valueOf(DATA_PORT)); - config.put("web.http.management.path", DATA_PATH); - config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); - config.put("web.http.protocol.path", PROTOCOL_PATH); - config.put("edc.api.auth.key", MANAGEMENT_API_KEY); - config.put("edc.ids.endpoint", PROTOCOL_ENDPOINT); - config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); - config.putAll(additionalConfigProperties); - return config; - } - - public static EdcClient edcClient() { - return EdcClient.builder() - .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) - .managementApiKey(TestUtils.MANAGEMENT_API_KEY) - .build(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index fc1bac49f..f287be931 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver; -import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import org.eclipse.edc.protocol.ids.api.configuration.IdsApiConfigurationExtension; @@ -83,8 +83,8 @@ private static Map getCoreEdcJdbcConfig(TestDatabase testDatabas return config; } - public static EdcClient edcClient() { - return EdcClient.builder() + public static BrokerServerClient edcClient() { + return BrokerServerClient.builder() .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) .managementApiKey(TestUtils.MANAGEMENT_API_KEY) .build(); From d1ec5276b64bf7bb6b4502b3fcf85c2dc54d2cf6 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 3 Jul 2023 17:19:07 +0200 Subject: [PATCH 081/295] fix: broker server ts client not working (#173) --- extensions/broker-server-api/client-ts/vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/client-ts/vite.config.ts b/extensions/broker-server-api/client-ts/vite.config.ts index acfae8c24..e8143fc31 100644 --- a/extensions/broker-server-api/client-ts/vite.config.ts +++ b/extensions/broker-server-api/client-ts/vite.config.ts @@ -8,8 +8,8 @@ export default defineConfig({ build: { lib: { entry: resolve(__dirname, 'src/index.ts'), - name: 'broker-server-client', - fileName: 'broker-server-client', + name: 'sovity-broker-server-client', + fileName: 'sovity-broker-server-client', }, sourcemap: true, }, From 34707465afc3b8bd62d3a2fdeca00e2a81901eaa Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:19:19 +0200 Subject: [PATCH 082/295] fix: PolicyDto after latest api-wrapper changes (#177) --- .../edc/ext/brokerserver/services/api/PolicyDtoBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java index f5961e73e..baffa368a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -24,6 +24,6 @@ public class PolicyDtoBuilder { @SneakyThrows public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { - return new PolicyDto(policyJson); + return new PolicyDto(policyJson, null); } } From f512aa8c43d117be83e713f2b1421fccdb39fd42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 07:49:03 +0200 Subject: [PATCH 083/295] chore(deps): bump io.swagger.core.v3:swagger-jaxrs2-jakarta (#182) Bumps io.swagger.core.v3:swagger-jaxrs2-jakarta from 2.2.14 to 2.2.15. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-jaxrs2-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 6a6983cf8..75ab8146d 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -18,14 +18,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.14") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.14") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.12.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.14") - implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.14") + implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") From 540c6bf53de1642f1c772c5fb728ddad44e284ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 07:54:32 +0200 Subject: [PATCH 084/295] chore(deps): bump io.swagger.core.v3:swagger-annotations-jakarta (#183) Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.14 to 2.2.15. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 75ab8146d..87d0a4541 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -17,14 +17,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.14") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.12.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.14") + implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") From a6e5d6c4a6a32e04dc0086909e42529ffeb0e51a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 08:00:06 +0200 Subject: [PATCH 085/295] chore(deps): bump io.swagger.core.v3.swagger-gradle-plugin (#181) Bumps io.swagger.core.v3.swagger-gradle-plugin from 2.2.14 to 2.2.15. --- updated-dependencies: - dependency-name: io.swagger.core.v3.swagger-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 87d0a4541..c5adc345e 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -4,7 +4,7 @@ val sovityEdcExtensionsVersion: String by project plugins { `java-library` `maven-publish` - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.14" //./gradlew clean resolve + id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.15" //./gradlew clean resolve id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI id("org.openapi.generator") version "6.6.0" //./gradlew openApiValidate && ./gradlew openApiGenerate } From 6cc9c40eaf3114942d84466bd0e7846ba984fb28 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 10 Jul 2023 09:29:27 +0200 Subject: [PATCH 086/295] fix: connectors starve if updates are too frequent (#176) --- .../BrokerServerExtensionContextBuilder.java | 4 +- .../services/queue/ThreadPool.java | 28 +++---- .../services/queue/ThreadPoolTask.java | 22 +++--- .../services/queue/ThreadPoolTaskQueue.java | 49 ++++++++++++ .../services/queue/ThreadPoolQueueTest.java | 47 +++++++++++ .../services/queue/ThreadPoolTest.java | 78 +++++++++++++++---- 6 files changed, 186 insertions(+), 42 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index db6e3c0d4..5f7f69871 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -45,6 +45,7 @@ import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; import de.sovity.edc.ext.brokerserver.services.queue.ThreadPool; +import de.sovity.edc.ext.brokerserver.services.queue.ThreadPoolTaskQueue; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; @@ -152,7 +153,8 @@ public static BrokerServerExtensionContext buildContext( var policyDtoBuilder = new PolicyDtoBuilder(); var assetPropertyParser = new AssetPropertyParser(objectMapper); var paginationMetadataUtils = new PaginationMetadataUtils(); - var threadPool = new ThreadPool(brokerServerSettings, monitor); + var threadPoolTaskQueue = new ThreadPoolTaskQueue(); + var threadPool = new ThreadPool(threadPoolTaskQueue, brokerServerSettings, monitor); var connectorQueue = new ConnectorQueue(connectorUpdater, threadPool); var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); var connectorCreator = new ConnectorCreator(connectorQueries); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java index 8de97bdf2..022751243 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java @@ -17,28 +17,30 @@ import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import org.eclipse.edc.spi.monitor.Monitor; -import java.util.ArrayList; -import java.util.Objects; import java.util.Set; -import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; public class ThreadPool { + private final ThreadPoolTaskQueue queue; + private final boolean enabled; - private final PriorityBlockingQueue queue; private final ThreadPoolExecutor threadPoolExecutor; - public ThreadPool(BrokerServerSettings brokerServerSettings, Monitor monitor) { - queue = new PriorityBlockingQueue<>(); - + public ThreadPool(ThreadPoolTaskQueue queue, BrokerServerSettings brokerServerSettings, Monitor monitor) { + this.queue = queue; int numThreads = brokerServerSettings.getNumThreads(); enabled = numThreads > 0; if (enabled) { monitor.info("Initializing ThreadPoolExecutor with %d threads.".formatted(numThreads)); - threadPoolExecutor = new ThreadPoolExecutor(numThreads, numThreads, 60, TimeUnit.SECONDS, queue); + threadPoolExecutor = new ThreadPoolExecutor( + numThreads, + numThreads, + 60, + TimeUnit.SECONDS, + queue.getAsRunnableQueue() + ); threadPoolExecutor.prestartAllCoreThreads(); } else { monitor.info("Skipped ThreadPoolExecutor initialization."); @@ -51,13 +53,7 @@ public void enqueueConnectorRefreshTask(int priority, Runnable runnable, String } public Set getQueuedConnectorEndpoints() { - var queuedRunnables = new ArrayList<>(queue); - - return queuedRunnables.stream().filter(ThreadPoolTask.class::isInstance) - .map(ThreadPoolTask.class::cast) - .map(ThreadPoolTask::getConnectorEndpoint) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); + return queue.getConnectorEndpoints(); } private void enqueueTask(ThreadPoolTask task) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java index 03c871309..ee69dd44f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java @@ -14,25 +14,29 @@ package de.sovity.edc.ext.brokerserver.services.queue; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.concurrent.atomic.AtomicLong; @Getter @RequiredArgsConstructor -@EqualsAndHashCode(of = {"priority", "connectorEndpoint"}) -public class ThreadPoolTask implements Comparable, Runnable { +public class ThreadPoolTask implements Runnable { + + public static final Comparator COMPARATOR = Comparator.comparing(ThreadPoolTask::getPriority) + .thenComparing(ThreadPoolTask::getSequence); + + /** + * {@link java.util.concurrent.PriorityBlockingQueue} does not guarantee sequential execution, so we need to add this. + */ + private static final AtomicLong SEQ = new AtomicLong(0); + private final long sequence = SEQ.incrementAndGet(); private final int priority; private final Runnable task; private final String connectorEndpoint; - @Override - public int compareTo(@NotNull ThreadPoolTask threadPoolTask) { - return priority - threadPoolTask.priority; - } - @Override public void run() { this.task.run(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java new file mode 100644 index 000000000..44123180d --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.queue; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +public class ThreadPoolTaskQueue { + + @Getter + private final PriorityBlockingQueue queue = new PriorityBlockingQueue<>(50, ThreadPoolTask.COMPARATOR); + + @SuppressWarnings("unchecked") + public PriorityBlockingQueue getAsRunnableQueue() { + return (PriorityBlockingQueue) (PriorityBlockingQueue) queue; + } + + public void add(ThreadPoolTask task) { + queue.add(task); + } + + public Set getConnectorEndpoints() { + var queuedRunnables = new ArrayList<>(queue); + + return queuedRunnables.stream() + .map(ThreadPoolTask::getConnectorEndpoint) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java new file mode 100644 index 000000000..55b2b1efa --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.queue; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThat; + +class ThreadPoolQueueTest { + + + /** + * Regression against bug where the queue did not act like a queue. + */ + @Test + void testOrdering() { + Runnable noop = () -> { + }; + + var queue = new ThreadPoolTaskQueue(); + queue.add(new ThreadPoolTask(1, noop, "1.0")); + queue.add(new ThreadPoolTask(2, noop, "2.0")); + queue.add(new ThreadPoolTask(1, noop, "1.1")); + queue.add(new ThreadPoolTask(2, noop, "2.1")); + queue.add(new ThreadPoolTask(0, noop, "0.0")); + + var result = new ArrayList(); + queue.getQueue().drainTo(result); + + assertThat(result).extracting(ThreadPoolTask::getConnectorEndpoint) + .containsExactly("0.0", "1.0", "1.1", "2.0", "2.1"); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java index b581cec1b..a45e23d0b 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java @@ -16,10 +16,15 @@ import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import lombok.SneakyThrows; +import org.apache.commons.lang3.Validate; import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -27,29 +32,70 @@ class ThreadPoolTest { + /** + * Regression against bug where parallelity wasn't actually enabled + */ @Test + @SneakyThrows void testParallelExecution() { - var monitor = mock(Monitor.class); - var brokerServerSettings = mock(BrokerServerSettings.class); - when(brokerServerSettings.getNumThreads()).thenReturn(2); - var threadPool = new ThreadPool(brokerServerSettings, monitor); + ThreadPool threadPool = newThreadPool(2); + var latch = new CountDownLatch(2); var result = new ArrayList(); - threadPool.enqueueConnectorRefreshTask(0, delay(100, () -> result.add("1")), "1"); - threadPool.enqueueConnectorRefreshTask(0, delay(50, () -> result.add("2")), "2"); - safeSleep(200); - assertThat(result).containsExactly("2", "1"); + var a = new BlockedRunnable(() -> { + result.add("a"); + latch.countDown(); + }); + var b = new BlockedRunnable(() -> { + result.add("b"); + latch.countDown(); + }); + + threadPool.enqueueConnectorRefreshTask(0, a, "a"); + threadPool.enqueueConnectorRefreshTask(0, b, "b"); + + b.release(); + Thread.sleep(250); // For some reason this is required + a.release(); + + Validate.isTrue(latch.await(500, TimeUnit.MILLISECONDS), "latch timed out"); + assertThat(result).containsExactly("b", "a"); } - Runnable delay(int delayInMs, Runnable onDone) { - return () -> { - safeSleep(delayInMs); - onDone.run(); - }; + + @NotNull + private ThreadPool newThreadPool(int numThreads) { + var monitor = mock(Monitor.class); + var brokerServerSettings = mock(BrokerServerSettings.class); + when(brokerServerSettings.getNumThreads()).thenReturn(numThreads); + var queue = new ThreadPoolTaskQueue(); + return new ThreadPool(queue, brokerServerSettings, monitor); } - @SneakyThrows - void safeSleep(int delayInMs) { - Thread.sleep(delayInMs); + private static class BlockedRunnable implements Runnable { + private static final Object GLOBAL_LOCK = new Object(); + private final Runnable runnable; + private final ReentrantLock lock = new ReentrantLock(); + + private BlockedRunnable(Runnable runnable) { + this.runnable = runnable; + lock.lock(); + } + + public void release() { + lock.unlock(); + } + + @Override + @SneakyThrows + public void run() { + var ok = lock.tryLock(10, TimeUnit.SECONDS); + Validate.isTrue(ok, "Program is stuck!"); + + // Prevent concurrency issues within the test code + synchronized (GLOBAL_LOCK) { + runnable.run(); + } + } } } From a7b98af0e8ecdb7a7cbcb2a0b0d1681c90e7354b Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:42:05 +0200 Subject: [PATCH 087/295] feat: HikariCP as connection pooling (#180) * feat: HikariCP * refactor: pr remarks * refactor: make pool size and connection timeout configurable * chore: restore some documentation * chore: fix tests * chore: fix pipeline --------- Co-authored-by: Richard Treier --- CHANGELOG.md | 6 +++ connector/.env | 6 +++ .../build.gradle.kts | 1 + .../brokerserver/db/DataSourceFactory.java | 54 ++++++++++--------- .../db/PostgresFlywayExtension.java | 5 +- .../services/api/PolicyDtoBuilder.java | 2 +- .../edc/ext/brokerserver/TestUtils.java | 2 + .../edc/ext/brokerserver/db/TestDatabase.java | 2 +- 8 files changed, 49 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f690671..2a5d0838a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,12 @@ Broker MvP using Core EDC MS8. # Pagination Configuration: Catalog Page Size (default: 20) EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE: 20 + + # Database Connection Pool Size + EDC_BROKER_SERVER_DB_CONNECTION_POOL_SIZE: 30 + + # Database Connection Timeout (in ms) + EDC_BROKER_SERVER_DB_CONNECTION_TIMEOUT_IN_MS: 30000 ``` 3. An issue prevented the keystore file from being read, preventing a successful data space log in. 4. Added a reference to [connector/.env](connector/.env) as source for other possible broker server configuration diff --git a/connector/.env b/connector/.env index 0071f104e..d8334d9b2 100644 --- a/connector/.env +++ b/connector/.env @@ -16,6 +16,12 @@ MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url MY_EDC_JDBC_USER=missing-postgresql-user MY_EDC_JDBC_PASSWORD=missing-postgresql-password +# Database Connection Pool Size +EDC_BROKER_SERVER_DB_CONNECTION_POOL_SIZE=30 + +# Database Connection Timeout (in ms) +EDC_BROKER_SERVER_DB_CONNECTION_TIMEOUT_IN_MS=30000 + # List of Connectors to be added on startup EDC_BROKER_SERVER_KNOWN_CONNECTORS= diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 131bae8d4..b6e64bbf8 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { jooqGenerator("org.postgresql:postgresql:42.6.0") flywayMigration("org.postgresql:postgresql:42.6.0") + implementation("com.zaxxer:HikariCP:5.0.1") annotationProcessor("org.projectlombok:lombok:1.18.28") compileOnly("org.projectlombok:lombok:1.18.28") diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java index 09b8627f7..7e2435ca0 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java @@ -14,24 +14,19 @@ package de.sovity.edc.ext.brokerserver.db; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import de.sovity.edc.ext.brokerserver.db.utils.JdbcCredentials; import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.persistence.EdcPersistenceException; import org.eclipse.edc.spi.system.configuration.Config; -import org.eclipse.edc.sql.datasource.ConnectionFactoryDataSource; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; import javax.sql.DataSource; -/** - * Create {@link DataSource}s from EDC Config. - */ @RequiredArgsConstructor public class DataSourceFactory { private final Config config; + /** * Create a new {@link DataSource} from EDC Config. * @@ -39,30 +34,37 @@ public class DataSourceFactory { */ public DataSource newDataSource() { var jdbcCredentials = JdbcCredentials.fromConfig(config); - return fromJdbcCredentials(jdbcCredentials); + int maxPoolSize = config.getInteger(PostgresFlywayExtension.DB_CONNECTION_POOL_SIZE); + int connectionTimeoutInMs = config.getInteger(PostgresFlywayExtension.DB_CONNECTION_TIMEOUT_IN_MS); + return newDataSource(jdbcCredentials, maxPoolSize, connectionTimeoutInMs); } /** - * Create a new {@link DataSource} from JDBC Credentials. + * Create a new {@link DataSource}. *
- * This method was extracted into a static method, so we can call it from our Test Code. + * This method is static, so we can use from test code. * - * @param jdbcCredentials jdbc credentials - * @return {@link DataSource} + * @param jdbcCredentials jdbc credentials + * @param maxPoolSize max pool size + * @param connectionTimeoutInMs connection timeout in ms + * @return {@link DataSource}. */ - public static DataSource fromJdbcCredentials(JdbcCredentials jdbcCredentials) { - return new ConnectionFactoryDataSource(() -> newConnection(jdbcCredentials)); - } + public static DataSource newDataSource( + JdbcCredentials jdbcCredentials, + int maxPoolSize, + int connectionTimeoutInMs + ) { + var hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(jdbcCredentials.jdbcUrl()); + hikariConfig.setUsername(jdbcCredentials.jdbcUser()); + hikariConfig.setPassword(jdbcCredentials.jdbcPassword()); + hikariConfig.setMinimumIdle(1); + hikariConfig.setMaximumPoolSize(maxPoolSize); + hikariConfig.setIdleTimeout(30000); + hikariConfig.setPoolName("edc-broker-server"); + hikariConfig.setMaxLifetime(50000); + hikariConfig.setConnectionTimeout(connectionTimeoutInMs); - private static Connection newConnection(JdbcCredentials jdbcCredentials) { - try { - return DriverManager.getConnection( - jdbcCredentials.jdbcUrl(), - jdbcCredentials.jdbcUser(), - jdbcCredentials.jdbcPassword() - ); - } catch (SQLException e) { - throw new EdcPersistenceException(e); - } + return new HikariDataSource(hikariConfig); } } diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java index e4107037c..f4ec17d7e 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java @@ -34,6 +34,10 @@ public class PostgresFlywayExtension implements ServiceExtension { public static final String FLYWAY_CLEAN_ENABLE = "edc.flyway.clean.enable"; @Setting public static final String FLYWAY_CLEAN = "edc.flyway.clean"; + @Setting + public static final String DB_CONNECTION_POOL_SIZE = "edc.broker.server.db.connection.pool.size"; + @Setting + public static final String DB_CONNECTION_TIMEOUT_IN_MS = "edc.broker.server.db.connection.timeout.in.ms"; @Provider public DataPlaneInstanceStatements dataPlaneInstanceStatements() { @@ -58,5 +62,4 @@ public void initialize(ServiceExtensionContext context) { var flywayMigrator = new FlywayMigrator(flyway, config, monitor); flywayMigrator.migrateAndRepair(); } - } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java index baffa368a..f5961e73e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -24,6 +24,6 @@ public class PolicyDtoBuilder { @SneakyThrows public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { - return new PolicyDto(policyJson, null); + return new PolicyDto(policyJson); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index f287be931..1f5046e77 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -58,6 +58,8 @@ public static Map createConfiguration( config.put(PostgresFlywayExtension.JDBC_URL, testDatabase.getJdbcUrl()); config.put(PostgresFlywayExtension.JDBC_USER, testDatabase.getJdbcUser()); config.put(PostgresFlywayExtension.JDBC_PASSWORD, testDatabase.getJdbcPassword()); + config.put(PostgresFlywayExtension.DB_CONNECTION_POOL_SIZE, "20"); + config.put(PostgresFlywayExtension.DB_CONNECTION_TIMEOUT_IN_MS, "3000"); config.put(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, "true"); config.put(PostgresFlywayExtension.FLYWAY_CLEAN, "true"); config.put(BrokerServerExtension.NUM_THREADS, "0"); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index 20e690f4b..e593df41c 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -46,7 +46,7 @@ default DslContextFactory getDslContextFactory() { */ default DataSource getDataSource() { var jdbcCredentials = new JdbcCredentials(getJdbcUrl(), getJdbcUser(), getJdbcPassword()); - return DataSourceFactory.fromJdbcCredentials(jdbcCredentials); + return DataSourceFactory.newDataSource(jdbcCredentials, 20, 30_000); } /** From 52ab0b2b51b53d87bc22d9e4663e6e8bbbdea9f1 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:47:28 +0200 Subject: [PATCH 088/295] feat: display connector crawling time (#171) Co-authored-by: Richard Treier --- .../api/model/ConnectorDetailPageResult.java | 3 ++ extensions/broker-server/build.gradle.kts | 2 +- .../connector/ConnectorPageQueryService.java | 26 ++++++++++--- .../connector/model/ConnectorDetailsRs.java | 37 +++++++++++++++++++ ...ectorRs.java => ConnectorListEntryRs.java} | 8 ++-- .../services/api/ConnectorApiService.java | 33 ++++++++++++++--- .../services/api/ConnectorApiTest.java | 24 ++++++++++-- 7 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/{ConnectorRs.java => ConnectorListEntryRs.java} (83%) diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java index 9cc08d915..3928045a8 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java @@ -50,4 +50,7 @@ public class ConnectorDetailPageResult { @Schema(description = "Number of known data offerings") private Integer numContractOffers; + + @Schema(description = "Average time to crawl the connector") + private Long connectorCrawlingTimeAvg; } diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 1b6994c48..cab342923 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { implementation("${edcGroup}:management-api-configuration:${edcVersion}") api(project(":extensions:broker-server-postgres-flyway-jooq")) - api(project(":extensions:broker-server-api:api")) + implementation(project(":extensions:broker-server-api:api")) implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java index ad22a2db4..0bde0093a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java @@ -15,9 +15,11 @@ package de.sovity.edc.ext.brokerserver.dao.pages.connector; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -28,24 +30,36 @@ import java.util.List; public class ConnectorPageQueryService { - public List queryConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { + public List queryConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of(c.ENDPOINT, c.CONNECTOR_ID)); + return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) .from(c) .where(filterBySearchQuery) + .groupBy(c.ENDPOINT) .orderBy(sortConnectorPage(c, sorting)) - .fetchInto(ConnectorRs.class); + .fetchInto(ConnectorListEntryRs.class); } - public ConnectorRs queryConnectorDetailPage(DSLContext dsl, String connectorEndpoint) { + public ConnectorDetailsRs queryConnectorDetailPage(DSLContext dsl, String connectorEndpoint) { var c = Tables.CONNECTOR; + var betm = Tables.BROKER_EXECUTION_TIME_MEASUREMENT; + var filterBySearchQuery = SearchUtils.simpleSearch(connectorEndpoint, List.of(c.ENDPOINT, c.CONNECTOR_ID)); - return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) + var avgSuccessfulCrawlTimeInMs = dsl.select(DSL.avg(betm.DURATION_IN_MS)) + .from(betm) + .where(betm.CONNECTOR_ENDPOINT.eq(connectorEndpoint), betm.ERROR_STATUS.eq(MeasurementErrorStatus.OK)) + .asField(); + + return dsl.select(c.asterisk(), + dataOfferCount(c.ENDPOINT).as("numDataOffers"), + avgSuccessfulCrawlTimeInMs.as("connectorCrawlingTimeAvg")) .from(c) .where(filterBySearchQuery) - .fetchOneInto(ConnectorRs.class); + .groupBy(c.ENDPOINT) + .fetchOneInto(ConnectorDetailsRs.class); } @NotNull diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java new file mode 100644 index 000000000..39d291c68 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.connector.model; + +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ConnectorDetailsRs { + String endpoint; + String connectorId; + OffsetDateTime createdAt; + OffsetDateTime lastSuccessfulRefreshAt; + OffsetDateTime lastRefreshAttemptAt; + ConnectorOnlineStatus onlineStatus; + Integer numDataOffers; + Long connectorCrawlingTimeAvg; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java similarity index 83% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorRs.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java index 97664ae8e..af82420a1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java @@ -25,12 +25,12 @@ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) -public class ConnectorRs { +public class ConnectorListEntryRs { String endpoint; String connectorId; - private OffsetDateTime createdAt; - private OffsetDateTime lastSuccessfulRefreshAt; - private OffsetDateTime lastRefreshAttemptAt; + OffsetDateTime createdAt; + OffsetDateTime lastSuccessfulRefreshAt; + OffsetDateTime lastRefreshAttemptAt; ConnectorOnlineStatus onlineStatus; Integer numDataOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 146f4e291..ca94b1b99 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -15,7 +15,8 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorRs; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorListEntry; @@ -52,7 +53,7 @@ public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDe Objects.requireNonNull(query, "query must not be null"); var connectorDbRow = connectorPageQueryService.queryConnectorDetailPage(dsl, query.getConnectorEndpoint()); - var connector = buildConnectorListEntry(connectorDbRow); + var connector = buildConnectorDetailPageEntry(connectorDbRow); var result = new ConnectorDetailPageResult(); result.setCreatedAt(connector.getCreatedAt()); @@ -62,14 +63,15 @@ public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDe result.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); result.setNumContractOffers(connector.getNumContractOffers()); result.setOnlineStatus(connector.getOnlineStatus()); + result.setConnectorCrawlingTimeAvg(connector.getConnectorCrawlingTimeAvg()); return result; } - private List buildConnectorListEntries(List connectors) { + private List buildConnectorListEntries(List connectors) { return connectors.stream().map(this::buildConnectorListEntry).toList(); } - private ConnectorListEntry buildConnectorListEntry(ConnectorRs connector) { + private ConnectorListEntry buildConnectorListEntry(ConnectorListEntryRs connector) { var dto = new ConnectorListEntry(); dto.setId(connector.getConnectorId()); dto.setEndpoint(connector.getEndpoint()); @@ -81,7 +83,28 @@ private ConnectorListEntry buildConnectorListEntry(ConnectorRs connector) { return dto; } - private ConnectorOnlineStatus getOnlineStatus(ConnectorRs connector) { + private ConnectorDetailPageResult buildConnectorDetailPageEntry(ConnectorDetailsRs connector) { + var dto = new ConnectorDetailPageResult(); + dto.setId(connector.getConnectorId()); + dto.setEndpoint(connector.getEndpoint()); + dto.setCreatedAt(connector.getCreatedAt()); + dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); + dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); + dto.setOnlineStatus(getOnlineStatus(connector)); + dto.setNumContractOffers(connector.getNumDataOffers()); + dto.setConnectorCrawlingTimeAvg(connector.getConnectorCrawlingTimeAvg()); + return dto; + } + + private ConnectorOnlineStatus getOnlineStatus(ConnectorListEntryRs connector) { + return switch (connector.getOnlineStatus()) { + case ONLINE -> ConnectorOnlineStatus.ONLINE; + case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + connector.getOnlineStatus()); + }; + } + + private ConnectorOnlineStatus getOnlineStatus(ConnectorDetailsRs connector) { return switch (connector.getOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index 44e414268..a57735a85 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -24,6 +24,9 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import lombok.SneakyThrows; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -72,7 +75,7 @@ void testQueryConnectors() { assertThat(result.getConnectors()).hasSize(1); var connector = result.getConnectors().get(0); - assertThat(connector.getId()).isEqualTo("http://my-connector"); + assertThat(connector.getId()).isEqualTo("http://my-connector/ids/data"); assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); @@ -86,6 +89,7 @@ void testQueryConnectorDetails() { var today = OffsetDateTime.now().withNano(0); createConnector(dsl, today, "http://my-connector/ids/data"); + createConnector(dsl, today, "http://my-connector2/ids/data"); createDataOffer(dsl, today, Map.of( AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", AssetProperty.DATA_CATEGORY, "my-category", @@ -93,17 +97,18 @@ void testQueryConnectorDetails() { ), "http://my-connector/ids/data"); var connector = edcClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("http://my-connector/ids/data")); - assertThat(connector.getId()).isEqualTo("http://my-connector"); + assertThat(connector.getId()).isEqualTo("http://my-connector/ids/data"); assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); + assertThat(connector.getConnectorCrawlingTimeAvg()).isEqualTo(150L); }); } private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setConnectorId("http://my-connector"); + connector.setConnectorId(connectorEndpoint); connector.setEndpoint(connectorEndpoint); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setCreatedAt(today.minusDays(1)); @@ -112,6 +117,19 @@ private void createConnector(DSLContext dsl, OffsetDateTime today, String connec connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); connector.insert(); + + addCrawlingTime(dsl, today, connector, 100L); + addCrawlingTime(dsl, today.plusHours(5), connector, 200L); + } + + private static void addCrawlingTime(DSLContext dsl, OffsetDateTime today, ConnectorRecord connector, Long duration) { + var crawlingTime = dsl.newRecord(Tables.BROKER_EXECUTION_TIME_MEASUREMENT); + crawlingTime.setConnectorEndpoint(connector.getEndpoint()); + crawlingTime.setDurationInMs(duration); + crawlingTime.setCreatedAt(today); + crawlingTime.setType(MeasurementType.CONNECTOR_REFRESH); + crawlingTime.setErrorStatus(MeasurementErrorStatus.OK); + crawlingTime.insert(); } private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { From 57206a97e338ab541b0180e8114a778d36aa3690 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 10 Jul 2023 11:04:24 +0200 Subject: [PATCH 089/295] chore: fix main after changes in edc-extensions (#186) --- .../edc/ext/brokerserver/services/api/PolicyDtoBuilder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java index f5961e73e..21c10adba 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -24,6 +24,8 @@ public class PolicyDtoBuilder { @SneakyThrows public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { - return new PolicyDto(policyJson); + var policyDto = new PolicyDto(); + policyDto.setLegacyPolicy(policyJson); + return policyDto; } } From 9c6eda2792fe5966cacd74fc35e7a5574a1c0e76 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:54:16 +0200 Subject: [PATCH 090/295] feat: log data offer page clicks (#160) Co-authored-by: Richard Treier --- .../api/model/DataOfferDetailPageResult.java | 3 +++ ... => V4_1__MvP_1_1_0_Non_Transactional.sql} | 0 .../db/migration/V4_2__MVP_1_1_0.sql | 8 +++++++ .../BrokerServerExtensionContextBuilder.java | 4 +++- .../dao/pages/catalog/CatalogQueryFields.java | 15 ++++++++++++- .../pages/catalog/CatalogQueryService.java | 1 + .../catalog/CatalogQuerySortingService.java | 5 +++++ .../DataOfferDetailPageQueryService.java | 4 +++- .../dao/pages/dataoffer/ViewCountLogger.java | 13 +++++++++++ .../dataoffer/model/DataOfferDetailRs.java | 1 + .../api/DataOfferDetailApiService.java | 8 +++++-- .../services/api/DataOfferDetailApiTest.java | 22 +++++++++++++++++-- 12 files changed, 77 insertions(+), 7 deletions(-) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V4_1__MvP_1_1_0.sql => V4_1__MvP_1_1_0_Non_Transactional.sql} (100%) create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java index 96a31edb2..bc40b36ea 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java @@ -55,4 +55,7 @@ public class DataOfferDetailPageResult { @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) private List contractOffers; + + @Schema(description = "View Count", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer viewCount; } diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0_Non_Transactional.sql similarity index 100% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0_Non_Transactional.sql diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql new file mode 100644 index 000000000..8935ebb22 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql @@ -0,0 +1,8 @@ +create table data_offer_view_count ( + id serial primary key, + connector_endpoint text not null, + asset_id text not null, + date timestamp with time zone not null +); + +create index data_offer_view_count_index on data_offer_view_count (connector_endpoint, asset_id); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 5f7f69871..6eab35ab8 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -25,6 +25,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQuerySortingService; import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.ViewCountLogger; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; @@ -165,8 +166,8 @@ public static BrokerServerExtensionContext buildContext( ); var catalogFilterAttributeDefinitionService = new CatalogFilterAttributeDefinitionService(); var catalogFilterService = new CatalogFilterService(catalogFilterAttributeDefinitionService); - var offlineConnectorRemover = new OfflineConnectorRemover(brokerServerSettings, connectorQueries, brokerEventLogger); + var viewCountLogger = new ViewCountLogger(); // Schedules List> jobs = List.of( @@ -206,6 +207,7 @@ public static BrokerServerExtensionContext buildContext( var dataOfferDetailApiService = new DataOfferDetailApiService( dataOfferDetailPageQueryService, + viewCountLogger, policyDtoBuilder, assetPropertyParser ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index 94bf912e7..68ae36b20 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOfferViewCount; import de.sovity.edc.ext.brokerserver.services.config.DataSpaceConfig; import lombok.AccessLevel; import lombok.Getter; @@ -37,6 +38,7 @@ public class CatalogQueryFields { Connector connectorTable; DataOffer dataOfferTable; + DataOfferViewCount dataOfferViewCountTable; // Asset Properties from JSON to be used in sorting / filtering Field assetId; @@ -51,9 +53,10 @@ public class CatalogQueryFields { DataSpaceConfig dataSpaceConfig; - public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable, DataSpaceConfig dataSpaceConfig) { + public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable, DataOfferViewCount dataOfferViewCountTable, DataSpaceConfig dataSpaceConfig) { this.connectorTable = connectorTable; this.dataOfferTable = dataOfferTable; + this.dataOfferViewCountTable = dataOfferViewCountTable; this.dataSpaceConfig = dataSpaceConfig; assetId = dataOfferTable.ASSET_ID; assetName = dataOfferTable.ASSET_NAME; @@ -93,7 +96,17 @@ public CatalogQueryFields withSuffix(String additionalSuffix) { return new CatalogQueryFields( connectorTable.as(connectorTable.getName() + "_" + additionalSuffix), dataOfferTable.as(dataOfferTable.getName() + "_" + additionalSuffix), + dataOfferViewCountTable.as(dataOfferViewCountTable.getName() + "_" + additionalSuffix), dataSpaceConfig ); } + + public Field getViewCount() { + var subquery = DSL.select(DSL.count()) + .from(dataOfferViewCountTable) + .where(dataOfferViewCountTable.ASSET_ID.eq(dataOfferTable.ASSET_ID) + .and(dataOfferViewCountTable.CONNECTOR_ENDPOINT.eq(connectorTable.ENDPOINT))); + + return subquery.asField(); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java index f1e46034b..04a585e35 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java @@ -51,6 +51,7 @@ public CatalogPageRs queryCatalogPage( var fields = new CatalogQueryFields( Tables.CONNECTOR, Tables.DATA_OFFER, + Tables.DATA_OFFER_VIEW_COUNT, brokerServerSettings.getDataSpaceConfig() ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java index d3661286a..b643d0f57 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java @@ -41,6 +41,11 @@ public List> getOrderBy(CatalogQueryFields fields, CatalogPageSort fields.getConnectorTable().ENDPOINT.asc(), fields.getAssetName().asc() ); + } else if (sorting == CatalogPageSortingType.VIEW_COUNT) { + orderBy = List.of( + fields.getViewCount().desc(), + fields.getConnectorTable().ENDPOINT.asc() + ); } else { throw new IllegalArgumentException("Unknown %s: %s".formatted(CatalogPageSortingType.class.getName(), sorting)); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java index e47e52829..7ca8a7d7c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java @@ -32,6 +32,7 @@ public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetI var fields = new CatalogQueryFields( Tables.CONNECTOR, Tables.DATA_OFFER, + Tables.DATA_OFFER_VIEW_COUNT, brokerServerSettings.getDataSpaceConfig() ); @@ -46,7 +47,8 @@ public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetI catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt"), c.ENDPOINT.as("connectorEndpoint"), - c.ONLINE_STATUS.as("connectorOnlineStatus")) + c.ONLINE_STATUS.as("connectorOnlineStatus"), + fields.getViewCount().as("viewCount")) .from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) .where(d.ASSET_ID.eq(assetId).or(d.CONNECTOR_ENDPOINT.eq(endpoint))) .fetchOneInto(DataOfferDetailRs.class); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java new file mode 100644 index 000000000..7fedc8f4c --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java @@ -0,0 +1,13 @@ +package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer; + +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; + +public class ViewCountLogger { + public void increaseDataOfferViewCount(DSLContext dsl, String assetId, String endpoint) { + var v = Tables.DATA_OFFER_VIEW_COUNT; + dsl.insertInto(v, v.ASSET_ID, v.CONNECTOR_ENDPOINT, v.DATE).values(assetId, endpoint, OffsetDateTime.now()).execute(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java index d0b6d4283..4d2cb5241 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java @@ -35,4 +35,5 @@ public class DataOfferDetailRs { String connectorEndpoint; ConnectorOnlineStatus connectorOnlineStatus; OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; + Integer viewCount; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java index 34ac2cfd0..aef62e66c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java @@ -15,11 +15,12 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailContractOffer; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.ViewCountLogger; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -30,6 +31,7 @@ @RequiredArgsConstructor public class DataOfferDetailApiService { private final DataOfferDetailPageQueryService dataOfferDetailPageQueryService; + private final ViewCountLogger viewCountLogger; private final PolicyDtoBuilder policyDtoBuilder; private final AssetPropertyParser assetPropertyParser; @@ -37,6 +39,7 @@ public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDe Objects.requireNonNull(query, "query must not be null"); var dataOffer = dataOfferDetailPageQueryService.queryDataOfferDetailsPage(dsl, query.getAssetId(), query.getConnectorEndpoint()); + viewCountLogger.increaseDataOfferViewCount(dsl, query.getAssetId(), query.getConnectorEndpoint()); var result = new DataOfferDetailPageResult(); result.setAssetId(dataOffer.getAssetId()); @@ -47,6 +50,7 @@ public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDe result.setCreatedAt(dataOffer.getCreatedAt()); result.setUpdatedAt(dataOffer.getUpdatedAt()); result.setContractOffers(buildDataOfferDetailContractOffers(dataOffer.getContractOffers())); + result.setViewCount(dataOffer.getViewCount()); return result; } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index 6eb4bef60..f58b527dc 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -76,9 +76,19 @@ void testQueryDataOfferDetails() { AssetProperty.ASSET_NAME, "My Asset 1" ), "http://my-connector/ids/data"); + //create view for dataoffer + createDataOfferView(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" + ), "http://my-connector/ids/data"); + createDataOfferView(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.ASSET_NAME, "My Asset 1" + ), "http://my-connector/ids/data"); var actual = edcClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("http://my-connector/ids/data", "urn:artifact:my-asset-1")); - assertThat(actual.getAssetId()).isEqualTo("urn:artifact:my-asset-1"); assertThat(actual.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); assertThat(actual.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); @@ -90,13 +100,13 @@ void testQueryDataOfferDetails() { AssetProperty.ASSET_NAME, "My Asset 1" )); assertThat(actual.getUpdatedAt()).isEqualTo(today); - assertThat(actual.getContractOffers()).hasSize(1); var contractOffer = actual.getContractOffers().get(0); assertThat(contractOffer.getContractOfferId()).isEqualTo("my-contract-offer-1"); //assertEqualJson(contractOffer.getContractPolicy().getLegacyPolicy(), policyToJson(dummyPolicy())); assertThat(contractOffer.getCreatedAt()).isEqualTo(today.minusDays(5)); assertThat(contractOffer.getUpdatedAt()).isEqualTo(today); + assertThat(actual.getViewCount()).isEqualTo(2); }); } @@ -133,6 +143,14 @@ private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { + var view = dsl.newRecord(Tables.DATA_OFFER_VIEW_COUNT); + view.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + view.setConnectorEndpoint(connectorEndpoint); + view.setDate(date); + view.insert(); + } + private Policy dummyPolicy() { return Policy.Builder.newInstance() .assignee("Example Assignee") From 209981b03d1e4123a44ab31dad58014fd32db34b Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:01:26 +0200 Subject: [PATCH 091/295] feat: connector list must be updatable (#166) * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * test: adapt tests to new DEAD onlinestatus * feat: add connectors only if not known * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * feat: connector list must be updatable * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * refactor: pr remarks * chore: license header * chore: license header * refactor: clean up ONLINE, OFFLINE, DEAD * chore: fix checkstyle * chore: format * chore: fix db script names * feat: sort connectors by online status (new default) * chore: fix name --------- Co-authored-by: Richard Treier --- CHANGELOG.md | 29 +++++++- connector/.env | 24 ++++-- docker-compose.yaml | 2 - .../api/BrokerServerResource.java | 9 +++ .../api/model/ConnectorOnlineStatus.java | 3 +- .../api/model/ConnectorPageSortingType.java | 1 + ...1_1_0.sql => V4_1__MvP_Bugfixes_1_1_0.sql} | 3 +- ... V4_2__MvP_Bugfixes_Non_Transactional.sql} | 3 +- .../brokerserver/BrokerServerExtension.java | 12 ++- .../BrokerServerExtensionContextBuilder.java | 74 +++++++++++++++---- .../BrokerServerResourceImpl.java | 8 ++ .../brokerserver/dao/ConnectorQueries.java | 7 +- .../connector/ConnectorPageQueryService.java | 11 ++- .../services/ConnectorCleaner.java | 30 ++++++++ .../services/ConnectorCreator.java | 3 +- .../services/ConnectorKiller.java | 29 ++++++++ .../services/OfflineConnectorKiller.java | 41 ++++++++++ .../services/OfflineConnectorRemover.java | 44 ----------- .../services/api/CatalogApiService.java | 11 +-- .../services/api/ConnectorApiService.java | 23 +++++- .../services/api/ConnectorService.java | 40 ++++++++++ .../api/DataOfferDetailApiService.java | 1 + .../services/api/PaginationMetadataUtils.java | 2 +- .../api/filtering/CatalogFilterService.java | 10 +-- .../services/config/BrokerServerSettings.java | 2 +- .../config/BrokerServerSettingsFactory.java | 4 +- .../services/logging/BrokerEventLogger.java | 6 +- .../services/queue/ConnectorQueueFiller.java | 7 +- .../queue/ConnectorRefreshPriority.java | 5 +- .../ConnectorUpdateSuccessWriter.java | 2 +- ...lJob.java => DeadConnectorRefreshJob.java} | 11 ++- ...ob.java => OfflineConnectorKillerJob.java} | 8 +- .../schedules/OfflineConnectorRefreshJob.java | 35 +++++++++ .../schedules/OnlineConnectorRefreshJob.java | 35 +++++++++ .../edc/ext/brokerserver/utils/UrlUtils.java | 12 +++ .../edc/ext/brokerserver/db/TestDatabase.java | 2 +- .../services/api/CatalogApiTest.java | 1 - .../services/api/DataOfferDetailApiTest.java | 1 - .../refreshing/ConnectorUpdaterTest.java | 4 +- .../OfflineConnectorRemovalJobTest.java | 36 +++++---- .../ext/brokerserver/utils/UrlUtilsTest.java | 33 +++++++++ 41 files changed, 489 insertions(+), 135 deletions(-) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V4_2__MVP_1_1_0.sql => V4_1__MvP_Bugfixes_1_1_0.sql} (73%) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V4_1__MvP_1_1_0_Non_Transactional.sql => V4_2__MvP_Bugfixes_Non_Transactional.sql} (53%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/{OfflineConnectorRemovalJob.java => DeadConnectorRefreshJob.java} (58%) rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/{ConnectorRefreshJob.java => OfflineConnectorKillerJob.java} (72%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5d0838a..ef56517b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deployment Migration Notes -1. There are new **optional** configuration properties: +1. Removed **optional** configuration properties: ```yaml - # Deletion of Connectors after they have been offline for a certain amount of time - EDC_BROKER_SERVER_DELETE_OFFLINE_CONNECTORS_AFTER=P5D - EDC_BROKER_SERVER_SCHEDULED_DELETE_OFFLINE_CONNECTORS=0 0 12 ? * * + # (Removed) CRON interval for crawling connectors + EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "0 */5 * ? * *" + ``` + +2. There are new **optional** configuration properties: + ```yaml + # CRON interval for crawling ONLINE connectors + EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH: "*/20 * * ? * *" + + # CRON interval for crawling OFFLINE connectors + EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH: "0 */5 * ? * *" + + # CRON interval for crawling DEAD connectors + EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH: "0 */60 */4 ? * *" + + # CRON interval for marking connectors as DEAD + EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS: "0 0 12 ? * *" + + # Delete data offers / mark as dead after connector has been offline for: + EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER: "P5D" + + # Hide data offers after connector has been offline for: + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "P1D" + ``` ## [v0.1.0] Broker MvP Release - 2023-06-23 diff --git a/connector/.env b/connector/.env index d8334d9b2..215b71037 100644 --- a/connector/.env +++ b/connector/.env @@ -32,14 +32,26 @@ EDC_BROKER_SERVER_DEFAULT_DATASPACE=MDS # e.g. Mobilithek=https://my-connector1/ids/data,SomeOtherDataspace=https://my-connector2/ids/data EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS= -# Frequency of refreshing connectors -EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH=0 */5 * ? * * +# CRON interval for crawling ONLINE connectors +EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH=*/20 * * ? * * -# Duration a data offer is shown for offline connectors +# CRON interval for crawling OFFLINE connectors +EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH=0 */5 * ? * * + +# CRON interval for crawling DEAD connectors +EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH=0 */60 */4 ? * * + +# CRON interval for marking connectors as DEAD +EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS=0 0 12 ? * * + +# Delete data offers / mark as dead after connector has been offline for: +EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER=P5D + +# Hide data offers after connector has been offline for: EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D # Parallelization for Crawling -EDC_BROKER_SERVER_NUM_THREADS=3 +EDC_BROKER_SERVER_NUM_THREADS=32 # Maximum number of Data Offers per Connector EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR=50 @@ -50,10 +62,6 @@ EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER=10 # Pagination Configuration: Catalog Page Size EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 -# Deletion of Connectors after they have been offline for a certain amount of time -EDC_BROKER_SERVER_DELETE_OFFLINE_CONNECTORS_AFTER=P5D -EDC_BROKER_SERVER_SCHEDULED_DELETE_OFFLINE_CONNECTORS=0 0 12 ? * * - # =========================================================== # Other EDC Config # =========================================================== diff --git a/docker-compose.yaml b/docker-compose.yaml index 7fc1eb498..298c4f68d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,8 +28,6 @@ services: EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' - EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "*/20 * * ? * *" # Update connectors every 20s in dev - EDC_BROKER_SERVER_NUM_THREADS: "16" EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" # Hide offline data offers after 1 minute in dev EDC_API_AUTH_KEY: "ApiKeyDefaultValue" # Management API Key (Access to UI should be secured by other means, as this key is sent to the UI) ports: diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index 0efd097ba..851da1a93 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -26,10 +26,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import java.util.List; + @Path("wrapper/broker") @Tag(name = "Broker Server", description = "Broker Server API Endpoints. Requires the Broker Server Extension") public interface BrokerServerResource { @@ -61,4 +64,10 @@ public interface BrokerServerResource { @Produces(MediaType.APPLICATION_JSON) @Operation(description = "Query a Known Connector's Detail Page") ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query); + + @PUT + @Path("connectors") + @Consumes + @Operation(description = "Add unknown Connectors to the Broker Server") + void addConnectors(List endpoints); } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java index 43700aea5..78295e455 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java @@ -19,6 +19,7 @@ @Schema(description = "Connector's online status") public enum ConnectorOnlineStatus { ONLINE, - OFFLINE + OFFLINE, + DEAD } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java index f110a5c6b..0cc284233 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java @@ -22,6 +22,7 @@ @RequiredArgsConstructor @Schema(description = "Connector List Page's known sorting option IDs") public enum ConnectorPageSortingType { + ONLINE_STATUS("Online Status"), MOST_RECENT("Most Recent"), TITLE("By Title"); diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql similarity index 73% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql index 8935ebb22..da600f9e5 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MVP_1_1_0.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql @@ -5,4 +5,5 @@ create table data_offer_view_count ( date timestamp with time zone not null ); -create index data_offer_view_count_index on data_offer_view_count (connector_endpoint, asset_id); +create index data_offer_view_count_speedup on data_offer_view_count (connector_endpoint, asset_id); + diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0_Non_Transactional.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql similarity index 53% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0_Non_Transactional.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql index a66d594d3..986e58cfd 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_1_1_0_Non_Transactional.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql @@ -1,4 +1,5 @@ -- Changes to Enums are non-transactional and must be supplied in a separate migration script for flyway -- Connector deleted due to being offline for too long -alter type broker_event_type add value 'CONNECTOR_DELETED_DUE_TO_OFFLINE_FOR_TOO_LONG'; +alter type broker_event_type add value 'CONNECTOR_KILLED_DUE_TO_OFFLINE_FOR_TOO_LONG'; +alter type connector_online_status add value 'DEAD'; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 15a986540..49f2b5d16 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -33,7 +33,13 @@ public class BrokerServerExtension implements ServiceExtension { public static final String KNOWN_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_CONNECTORS"); @Setting - public static final String CRON_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH"); + public static final String CRON_ONLINE_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH"); + + @Setting + public static final String CRON_OFFLINE_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH"); + + @Setting + public static final String CRON_DEAD_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH"); @Setting public static final String NUM_THREADS = toEdcProp("EDC_BROKER_SERVER_NUM_THREADS"); @@ -57,10 +63,10 @@ public class BrokerServerExtension implements ServiceExtension { public static final String KNOWN_DATASPACE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS"); @Setting - public static final String DELETE_OFFLINE_CONNECTORS_AFTER = toEdcProp("EDC_BROKER_SERVER_DELETE_OFFLINE_CONNECTORS_AFTER"); + public static final String KILL_OFFLINE_CONNECTORS_AFTER = toEdcProp("EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER"); @Setting - public static final String SCHEDULED_DELETE_OFFLINE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_SCHEDULED_DELETE_OFFLINE_CONNECTORS"); + public static final String SCHEDULED_KILL_OFFLINE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS"); @Inject private ManagementApiConfiguration managementApiConfiguration; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 6eab35ab8..40511d100 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -29,12 +29,15 @@ import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; +import de.sovity.edc.ext.brokerserver.services.ConnectorCleaner; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; +import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; -import de.sovity.edc.ext.brokerserver.services.OfflineConnectorRemover; +import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; @@ -59,8 +62,10 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchBuilder; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; -import de.sovity.edc.ext.brokerserver.services.schedules.ConnectorRefreshJob; -import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorRemovalJob; +import de.sovity.edc.ext.brokerserver.services.schedules.DeadConnectorRefreshJob; +import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorKillerJob; +import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorRefreshJob; +import de.sovity.edc.ext.brokerserver.services.schedules.OnlineConnectorRefreshJob; import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; import lombok.NoArgsConstructor; @@ -69,6 +74,7 @@ import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.TypeManager; +import org.jetbrains.annotations.NotNull; import java.util.List; @@ -166,21 +172,24 @@ public static BrokerServerExtensionContext buildContext( ); var catalogFilterAttributeDefinitionService = new CatalogFilterAttributeDefinitionService(); var catalogFilterService = new CatalogFilterService(catalogFilterAttributeDefinitionService); - var offlineConnectorRemover = new OfflineConnectorRemover(brokerServerSettings, connectorQueries, brokerEventLogger); var viewCountLogger = new ViewCountLogger(); + var connectorService = new ConnectorService(connectorCreator, connectorQueue); + var connectorKiller = new ConnectorKiller(); + var connectorClearer = new ConnectorCleaner(); + var offlineConnectorKiller = new OfflineConnectorKiller( + brokerServerSettings, + connectorQueries, + brokerEventLogger, + connectorKiller, + connectorClearer + ); // Schedules List> jobs = List.of( - new CronJobRef<>( - BrokerServerExtension.CRON_CONNECTOR_REFRESH, - ConnectorRefreshJob.class, - () -> new ConnectorRefreshJob(dslContextFactory, connectorQueueFiller) - ), - new CronJobRef<>( - BrokerServerExtension.SCHEDULED_DELETE_OFFLINE_CONNECTORS, - OfflineConnectorRemovalJob.class, - () -> new OfflineConnectorRemovalJob(dslContextFactory, offlineConnectorRemover) - ) + getOnlineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getOfflineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getDeadConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getOfflineConnectorKillerCronJob(dslContextFactory, offlineConnectorKiller) ); // Startup @@ -202,6 +211,7 @@ public static BrokerServerExtensionContext buildContext( ); var connectorApiService = new ConnectorApiService( connectorPageQueryService, + connectorService, paginationMetadataUtils ); @@ -226,4 +236,40 @@ public static BrokerServerExtensionContext buildContext( connectorCreator ); } + + @NotNull + private static CronJobRef getOfflineConnectorKillerCronJob(DslContextFactory dslContextFactory, OfflineConnectorKiller offlineConnectorKiller) { + return new CronJobRef<>( + BrokerServerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, + OfflineConnectorKillerJob.class, + () -> new OfflineConnectorKillerJob(dslContextFactory, offlineConnectorKiller) + ); + } + + @NotNull + private static CronJobRef getOnlineConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { + return new CronJobRef<>( + BrokerServerExtension.CRON_ONLINE_CONNECTOR_REFRESH, + OnlineConnectorRefreshJob.class, + () -> new OnlineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ); + } + + @NotNull + private static CronJobRef getOfflineConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { + return new CronJobRef<>( + BrokerServerExtension.CRON_OFFLINE_CONNECTOR_REFRESH, + OfflineConnectorRefreshJob.class, + () -> new OfflineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ); + } + + @NotNull + private static CronJobRef getDeadConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { + return new CronJobRef<>( + BrokerServerExtension.CRON_DEAD_CONNECTOR_REFRESH, + DeadConnectorRefreshJob.class, + () -> new DeadConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index c24f42ed6..d22d1c756 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -29,6 +29,8 @@ import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import lombok.RequiredArgsConstructor; +import java.util.List; + /** * Implementation of {@link BrokerServerResource} @@ -59,4 +61,10 @@ public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery qu public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query) { return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorDetailPage(dsl, query)); } + + @Override + public void addConnectors(List endpoints) { + dslContextFactory.transaction(dsl -> connectorApiService.addConnectors(dsl, endpoints)); + } + } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java index f1510a536..31fd44a7f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java @@ -16,6 +16,7 @@ import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import org.jooq.DSLContext; @@ -32,9 +33,9 @@ public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); } - public Set findConnectorsForScheduledRefresh(DSLContext dsl) { + public Set findConnectorsForScheduledRefresh(DSLContext dsl, ConnectorOnlineStatus onlineStatus) { var c = Tables.CONNECTOR; - return dsl.select(c.ENDPOINT).from(c).fetchSet(c.ENDPOINT); + return dsl.select(c.ENDPOINT).from(c).where(c.ONLINE_STATUS.eq(onlineStatus)).fetchSet(c.ENDPOINT); } public Set findExistingConnectors(DSLContext dsl, Collection connectorEndpoints) { @@ -44,7 +45,7 @@ public Set findExistingConnectors(DSLContext dsl, Collection con .fetchSet(c.ENDPOINT); } - public List findAllConnectorsForDeletion(DSLContext dsl, Duration deleteOfflineConnectorsAfter) { + public List findAllConnectorsForKilling(DSLContext dsl, Duration deleteOfflineConnectorsAfter) { var c = Tables.CONNECTOR; return dsl.select(c.ENDPOINT).from(c) .where(c.LAST_SUCCESSFUL_REFRESH_AT.lt(OffsetDateTime.now().minus(deleteOfflineConnectorsAfter))) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java index 0bde0093a..15b91fb3b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java @@ -19,6 +19,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import org.jetbrains.annotations.NotNull; @@ -37,7 +38,6 @@ public List queryConnectorPage(DSLContext dsl, String sear return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) .from(c) .where(filterBySearchQuery) - .groupBy(c.ENDPOINT) .orderBy(sortConnectorPage(c, sorting)) .fetchInto(ConnectorListEntryRs.class); } @@ -66,8 +66,15 @@ public ConnectorDetailsRs queryConnectorDetailPage(DSLContext dsl, String connec private List> sortConnectorPage(Connector c, ConnectorPageSortingType sorting) { var alphabetically = c.ENDPOINT.asc(); var recentFirst = c.CREATED_AT.desc(); + var onlineStatus = DSL.case_(c.ONLINE_STATUS) + .when(ConnectorOnlineStatus.ONLINE, 1) + .when(ConnectorOnlineStatus.OFFLINE, 2) + .else_(3) + .asc(); - if (sorting == null || sorting == ConnectorPageSortingType.TITLE) { + if (sorting == null || sorting == ConnectorPageSortingType.ONLINE_STATUS) { + return List.of(onlineStatus, alphabetically); + } else if (sorting == ConnectorPageSortingType.TITLE) { return List.of(alphabetically, recentFirst); } else if (sorting == ConnectorPageSortingType.MOST_RECENT) { return List.of(recentFirst, alphabetically); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java new file mode 100644 index 000000000..512fdbd6e --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import org.jooq.DSLContext; + +import java.util.Collection; + +public class ConnectorCleaner { + public void removeDataForDeadConnectors(DSLContext dsl, Collection endpoints) { + var doco = Tables.DATA_OFFER_CONTRACT_OFFER; + var dof = Tables.DATA_OFFER; + dsl.deleteFrom(doco).where(PostgresqlUtils.in(doco.CONNECTOR_ENDPOINT, endpoints)).execute(); + dsl.deleteFrom(dof).where(PostgresqlUtils.in(dof.CONNECTOR_ENDPOINT, endpoints)).execute(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index 8d87f2c51..ae83ced18 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -26,6 +26,7 @@ import org.jooq.DSLContext; import java.time.OffsetDateTime; +import java.util.Collection; import java.util.List; @RequiredArgsConstructor @@ -36,7 +37,7 @@ public void addConnector(DSLContext dsl, String connectorEndpoint) { addConnectors(dsl, List.of(connectorEndpoint)); } - public void addConnectors(DSLContext dsl, List connectorEndpoints) { + public void addConnectors(DSLContext dsl, Collection connectorEndpoints) { // Don't create connectors that already exist var existingConnectors = connectorQueries.findExistingConnectors(dsl, connectorEndpoints); var newConnectors = CollectionUtils2.difference(connectorEndpoints, existingConnectors); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java new file mode 100644 index 000000000..f44353fdb --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import org.jooq.DSLContext; + +import java.util.Collection; + +public class ConnectorKiller { + public void killConnectors(DSLContext dsl, Collection endpoints) { + var c = Tables.CONNECTOR; + dsl.update(c).set(c.ONLINE_STATUS, ConnectorOnlineStatus.DEAD).where(PostgresqlUtils.in(c.ENDPOINT, endpoints)).execute(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java new file mode 100644 index 000000000..c459269e7 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + + +package de.sovity.edc.ext.brokerserver.services; + +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class OfflineConnectorKiller { + private final BrokerServerSettings brokerServerSettings; + private final ConnectorQueries connectorQueries; + private final BrokerEventLogger brokerEventLogger; + private final ConnectorKiller connectorKiller; + private final ConnectorCleaner connectorClearer; + + public void killIfOfflineTooLong(DSLContext dsl) { + var killOfflineConnectorsAfter = brokerServerSettings.getKillOfflineConnectorsAfter(); + var toKill = connectorQueries.findAllConnectorsForKilling(dsl, killOfflineConnectorsAfter); + + connectorClearer.removeDataForDeadConnectors(dsl, toKill); + connectorKiller.killConnectors(dsl, toKill); + + brokerEventLogger.addKilledDueToOfflineTooLongMessages(dsl, toKill); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java deleted file mode 100644 index 30131b75d..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorRemover.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -@RequiredArgsConstructor -public class OfflineConnectorRemover { - private final BrokerServerSettings brokerServerSettings; - private final ConnectorQueries connectorQueries; - private final BrokerEventLogger brokerEventLogger; - - public void removeIfOfflineTooLong(DSLContext dsl) { - var deleteOfflineConnectorsAfter = brokerServerSettings.getDeleteOfflineConnectorsAfter(); - var toDelete = connectorQueries.findAllConnectorsForDeletion(dsl, deleteOfflineConnectorsAfter); - - // delete in batches, child entities first. - dsl.deleteFrom(Tables.DATA_OFFER_CONTRACT_OFFER).where(PostgresqlUtils.in(Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, toDelete)).execute(); - dsl.deleteFrom(Tables.DATA_OFFER).where(PostgresqlUtils.in(Tables.DATA_OFFER.CONNECTOR_ENDPOINT, toDelete)).execute(); - dsl.deleteFrom(Tables.CONNECTOR).where(PostgresqlUtils.in(Tables.CONNECTOR.ENDPOINT, toDelete)).execute(); - - // add log messages - brokerEventLogger.addDeletedDueToOfflineTooLongMessages(dsl, toDelete); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index c25e32644..e7d986578 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -14,11 +14,6 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; -import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.api.model.CatalogContractOffer; import de.sovity.edc.ext.brokerserver.api.model.CatalogDataOffer; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; @@ -26,6 +21,11 @@ import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingItem; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; +import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; +import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -115,6 +115,7 @@ private ConnectorOnlineStatus getOnlineStatus(DataOfferListEntryRs dataOfferRs) return switch (dataOfferRs.getConnectorOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + case DEAD -> ConnectorOnlineStatus.DEAD; default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + dataOfferRs.getConnectorOnlineStatus()); }; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index ca94b1b99..85fc69ca9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -14,9 +14,6 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorListEntry; @@ -25,6 +22,10 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingItem; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; +import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -32,9 +33,13 @@ import java.util.Objects; import java.util.stream.Stream; +import static de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority.ADDED_ON_API_CALL; +import static java.util.stream.Collectors.toSet; + @RequiredArgsConstructor public class ConnectorApiService { private final ConnectorPageQueryService connectorPageQueryService; + private final ConnectorService connectorService; private final PaginationMetadataUtils paginationMetadataUtils; public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery query) { @@ -100,6 +105,7 @@ private ConnectorOnlineStatus getOnlineStatus(ConnectorListEntryRs connector) { return switch (connector.getOnlineStatus()) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + case DEAD -> ConnectorOnlineStatus.DEAD; default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + connector.getOnlineStatus()); }; } @@ -118,4 +124,15 @@ private List buildAvailableSortings() { ConnectorPageSortingType.TITLE ).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); } + + public void addConnectors(DSLContext dsl, List connectorEndpoints) { + var existingEndpoints = connectorService.getConnectorEndpoints(dsl); + var endpoints = connectorEndpoints.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(UrlUtils::isValidUrl) + .filter(endpoint -> !existingEndpoints.contains(endpoint)) + .collect(toSet()); + connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java new file mode 100644 index 000000000..0b357557c --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.Collection; +import java.util.Set; + +import static de.sovity.edc.ext.brokerserver.db.jooq.Tables.CONNECTOR; + +@RequiredArgsConstructor +public class ConnectorService { + private final ConnectorCreator connectorCreator; + private final ConnectorQueue connectorQueue; + + public void addConnectors(DSLContext dsl, Collection connectorEndpoints, int priority) { + connectorCreator.addConnectors(dsl, connectorEndpoints); + connectorQueue.addAll(connectorEndpoints, priority); + } + + public Set getConnectorEndpoints(DSLContext dsl) { + return dsl.select(CONNECTOR.ENDPOINT).from(CONNECTOR).fetchSet(CONNECTOR.ENDPOINT); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java index aef62e66c..137743c26 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java @@ -64,6 +64,7 @@ private ConnectorOnlineStatus mapConnectorOnlineStatus( return switch (connectorOnlineStatus) { case ONLINE -> ConnectorOnlineStatus.ONLINE; case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + case DEAD -> ConnectorOnlineStatus.DEAD; }; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java index 16ed30725..d69cbb707 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java @@ -14,8 +14,8 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import de.sovity.edc.ext.brokerserver.api.model.PaginationMetadata; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index cc68b5ff6..f59b312fa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -14,16 +14,16 @@ package de.sovity.edc.ext.brokerserver.services.api.filtering; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; -import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; -import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import de.sovity.edc.ext.brokerserver.api.model.CnfFilter; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterAttribute; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterItem; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValue; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValueAttribute; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; +import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; +import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java index 120cccb20..beec07b01 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java @@ -30,5 +30,5 @@ public class BrokerServerSettings { int numThreads; - Duration deleteOfflineConnectorsAfter; + Duration killOfflineConnectorsAfter; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java index 504c5ee74..bbc6486ed 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -35,14 +35,14 @@ public BrokerServerSettings buildBrokerServerSettings() { var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); var dataSpaceConfig = buildDataSpaceConfig(config); var numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); - var deleteOfflineConnectorsAfter = getDuration(BrokerServerExtension.DELETE_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); + var killOfflineConnectorsAfter = getDuration(BrokerServerExtension.KILL_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); return BrokerServerSettings.builder() .hideOfflineDataOffersAfter(hideOfflineDataOffersAfter) .catalogPagePageSize(catalogPagePageSize) .dataSpaceConfig(dataSpaceConfig) .numThreads(numThreads) - .deleteOfflineConnectorsAfter(deleteOfflineConnectorsAfter) + .killOfflineConnectorsAfter(killOfflineConnectorsAfter) .build(); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index 2b9889dee..f97c48bab 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -102,13 +102,13 @@ public void logConnectorUpdateContractOfferLimitOk(String endpoint) { logEntry.insert(); } - public void addDeletedDueToOfflineTooLongMessages(DSLContext dsl, List deletedConnectorEndpoints) { + public void addKilledDueToOfflineTooLongMessages(DSLContext dsl, List deletedConnectorEndpoints) { var logEntries = deletedConnectorEndpoints.stream().map(endpoint -> { var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_DELETED_DUE_TO_OFFLINE_FOR_TOO_LONG); + logEntry.setEvent(BrokerEventType.CONNECTOR_KILLED_DUE_TO_OFFLINE_FOR_TOO_LONG); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage("Connector was removed for being offline too long."); + logEntry.setUserMessage("Connector was marked as dead for being offline too long."); logEntry.setConnectorEndpoint(endpoint); return logEntry; }).collect(Collectors.toList()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java index d03f58ffa..e85a652e4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.services.queue; import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -23,8 +24,8 @@ public class ConnectorQueueFiller { private final ConnectorQueue connectorQueue; private final ConnectorQueries connectorQueries; - public void enqueueAllConnectors(DSLContext dsl) { - var endpoints = connectorQueries.findConnectorsForScheduledRefresh(dsl); - connectorQueue.addAll(endpoints, ConnectorRefreshPriority.SCHEDULED_REFRESH); + public void enqueueConnectors(DSLContext dsl, ConnectorOnlineStatus status, int priority) { + var endpoints = connectorQueries.findConnectorsForScheduledRefresh(dsl, status); + connectorQueue.addAll(endpoints, priority); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java index 8933913c2..6931aad63 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java @@ -20,5 +20,8 @@ public class ConnectorRefreshPriority { public static final int ADMIN_REQUESTED = 1; public static final int ADDED_ON_STARTUP = 10; - public static final int SCHEDULED_REFRESH = 100; + public static final int ADDED_ON_API_CALL = 50; + public static final int SCHEDULED_ONLINE_REFRESH = 100; + public static final int SCHEDULED_OFFLINE_REFRESH = 200; + public static final int SCHEDULED_DEAD_REFRESH = 300; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 40aafe6b1..83c61889b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -45,7 +45,7 @@ public void handleConnectorOnline( dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, limitedDataOffers); // Log Status Change and set status to online if necessary - if (connector.getOnlineStatus() == ConnectorOnlineStatus.OFFLINE || connector.getLastRefreshAttemptAt() == null) { + if (connector.getOnlineStatus() != ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { brokerEventLogger.logConnectorOnline(dsl, connector.getEndpoint()); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/DeadConnectorRefreshJob.java similarity index 58% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/DeadConnectorRefreshJob.java index 373347482..63caca686 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJob.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/DeadConnectorRefreshJob.java @@ -15,18 +15,21 @@ package de.sovity.edc.ext.brokerserver.services.schedules; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.OfflineConnectorRemover; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; import lombok.RequiredArgsConstructor; import org.quartz.Job; import org.quartz.JobExecutionContext; @RequiredArgsConstructor -public class OfflineConnectorRemovalJob implements Job { +public class DeadConnectorRefreshJob implements Job { private final DslContextFactory dslContextFactory; - private final OfflineConnectorRemover offlineConnectorRemover; + private final ConnectorQueueFiller connectorQueueFiller; @Override public void execute(JobExecutionContext context) { - dslContextFactory.transaction(offlineConnectorRemover::removeIfOfflineTooLong); + dslContextFactory.transaction(dsl -> connectorQueueFiller.enqueueConnectors(dsl, + ConnectorOnlineStatus.DEAD, ConnectorRefreshPriority.SCHEDULED_DEAD_REFRESH)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java similarity index 72% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java index fde62b166..f79826482 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/ConnectorRefreshJob.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java @@ -15,18 +15,18 @@ package de.sovity.edc.ext.brokerserver.services.schedules; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; import lombok.RequiredArgsConstructor; import org.quartz.Job; import org.quartz.JobExecutionContext; @RequiredArgsConstructor -public class ConnectorRefreshJob implements Job { +public class OfflineConnectorKillerJob implements Job { private final DslContextFactory dslContextFactory; - private final ConnectorQueueFiller connectorQueueFiller; + private final OfflineConnectorKiller offlineConnectorKiller; @Override public void execute(JobExecutionContext context) { - dslContextFactory.transaction(connectorQueueFiller::enqueueAllConnectors); + dslContextFactory.transaction(offlineConnectorKiller::killIfOfflineTooLong); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java new file mode 100644 index 000000000..5cbeca52d --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules; + +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; +import lombok.RequiredArgsConstructor; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +public class OfflineConnectorRefreshJob implements Job { + private final DslContextFactory dslContextFactory; + private final ConnectorQueueFiller connectorQueueFiller; + + @Override + public void execute(JobExecutionContext context) { + dslContextFactory.transaction(dsl -> connectorQueueFiller.enqueueConnectors(dsl, + ConnectorOnlineStatus.OFFLINE, ConnectorRefreshPriority.SCHEDULED_OFFLINE_REFRESH)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java new file mode 100644 index 000000000..839a62321 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.schedules; + +import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; +import lombok.RequiredArgsConstructor; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +public class OnlineConnectorRefreshJob implements Job { + private final DslContextFactory dslContextFactory; + private final ConnectorQueueFiller connectorQueueFiller; + + @Override + public void execute(JobExecutionContext context) { + dslContextFactory.transaction(dsl -> connectorQueueFiller.enqueueConnectors(dsl, + ConnectorOnlineStatus.ONLINE, ConnectorRefreshPriority.SCHEDULED_ONLINE_REFRESH)); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java index d32425267..f728d05ef 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java @@ -17,7 +17,10 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class UrlUtils { @@ -43,4 +46,13 @@ public static String getEverythingBeforeThePath(String url) { } return everythingBeforePath; } + + public static boolean isValidUrl(String url) { + try { + new URL(url).toURI(); + return true; + } catch (MalformedURLException | URISyntaxException e) { + return false; + } + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index e593df41c..bf200c29d 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -19,8 +19,8 @@ import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; -import javax.sql.DataSource; import java.util.function.Consumer; +import javax.sql.DataSource; public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { String getJdbcUrl(); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 0e4ebe5a2..8a39af447 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -46,7 +46,6 @@ import java.util.Map; import java.util.stream.IntStream; -import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index f58b527dc..aa1cd06f2 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -38,7 +38,6 @@ import java.time.OffsetDateTime; import java.util.Map; -import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; import static groovy.json.JsonOutput.toJson; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index d226f9f69..b3eda5549 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -122,8 +122,8 @@ private void createAsset( .build(); var dataAddress = DataAddress.Builder.newInstance() .properties(Map.of( - "type", "HttpData", - "baseUrl", "https://jsonplaceholder.typicode.com/todos/1" + "type", "HttpData", + "baseUrl", "https://jsonplaceholder.typicode.com/todos/1" )) .build(); assetService.create(asset, dataAddress); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java index 018d674da..25f51fd7a 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java @@ -22,7 +22,9 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.services.OfflineConnectorRemover; +import de.sovity.edc.ext.brokerserver.services.ConnectorCleaner; +import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; +import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import org.jooq.DSLContext; @@ -45,7 +47,7 @@ class OfflineConnectorRemovalJobTest { private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); BrokerServerSettings brokerServerSettings; - OfflineConnectorRemover offlineConnectorRemover; + OfflineConnectorKiller offlineConnectorKiller; @BeforeAll static void beforeAll() { @@ -55,40 +57,48 @@ static void beforeAll() { @BeforeEach void beforeEach() { brokerServerSettings = mock(BrokerServerSettings.class); - offlineConnectorRemover = new OfflineConnectorRemover( - brokerServerSettings, + offlineConnectorKiller = new OfflineConnectorKiller( + brokerServerSettings, new ConnectorQueries(), - new BrokerEventLogger() + new BrokerEventLogger(), + new ConnectorKiller(), + new ConnectorCleaner() ); } @Test - void test_offlineConnectorRemoval_should_remove() { + void test_offlineConnectorKiller_should_be_dead() { TEST_DATABASE.testTransaction(dsl -> { // arrange - when(brokerServerSettings.getDeleteOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); + when(brokerServerSettings.getKillOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); createConnector(dsl, 6); // act - offlineConnectorRemover.removeIfOfflineTooLong(dsl); + offlineConnectorKiller.killIfOfflineTooLong(dsl); // assert - assertThat(dsl.selectCount().from(CONNECTOR).fetchOne(0, Integer.class)).isZero(); + dsl.select().from(CONNECTOR).fetch().forEach(record -> { + assertThat(record.get(CONNECTOR.CONNECTOR_ID)).isEqualTo("http://example.org"); + assertThat(record.get(CONNECTOR.ONLINE_STATUS)).isEqualTo(ConnectorOnlineStatus.DEAD); + }); }); } @Test - void test_offlineConnectorRemoval_should_not_remove() { + void test_offlineConnectorKiller_should_not_be_dead() { TEST_DATABASE.testTransaction(dsl -> { // arrange - when(brokerServerSettings.getDeleteOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); + when(brokerServerSettings.getKillOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); createConnector(dsl, 2); // act - offlineConnectorRemover.removeIfOfflineTooLong(dsl); + offlineConnectorKiller.killIfOfflineTooLong(dsl); // assert - assertThat(dsl.selectCount().from(CONNECTOR).fetchOne(0, Integer.class)).isNotZero(); + dsl.select().from(CONNECTOR).fetch().forEach(record -> { + assertThat(record.get(CONNECTOR.CONNECTOR_ID)).isEqualTo("http://example.org"); + assertThat(record.get(CONNECTOR.ONLINE_STATUS)).isNotEqualTo(ConnectorOnlineStatus.DEAD); + }); }); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java new file mode 100644 index 000000000..72a223007 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UrlUtilsTest { + @Test + void test_urlUtils() { + assertTrue(UrlUtils.isValidUrl("http://localhost:8080")); + assertTrue(UrlUtils.isValidUrl(" http://localhost:8080")); + + assertFalse(UrlUtils.isValidUrl("test")); + assertFalse(UrlUtils.isValidUrl("")); + assertFalse(UrlUtils.isValidUrl(" ")); + assertFalse(UrlUtils.isValidUrl(null)); + } +} From a4860cffec01b5fdcb7e7daab960b9df0469637d Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 12 Jul 2023 09:15:29 +0200 Subject: [PATCH 092/295] fix: .env cron expressions (#191) --- CHANGELOG.md | 8 ++++---- connector/.env | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef56517b7..5568bdfe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,16 +33,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 2. There are new **optional** configuration properties: ```yaml # CRON interval for crawling ONLINE connectors - EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH: "*/20 * * ? * *" + EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH: "*/20 * * ? * *" # every 20s # CRON interval for crawling OFFLINE connectors - EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH: "0 */5 * ? * *" + EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH: "0 */5 * ? * *" # every 5 minutes # CRON interval for crawling DEAD connectors - EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH: "0 */60 */4 ? * *" + EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH: "0 0 * ? * *" # every hour # CRON interval for marking connectors as DEAD - EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS: "0 0 12 ? * *" + EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS: "0 0 2 ? * *" # every day at 2am # Delete data offers / mark as dead after connector has been offline for: EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER: "P5D" diff --git a/connector/.env b/connector/.env index 215b71037..b52a92f98 100644 --- a/connector/.env +++ b/connector/.env @@ -39,10 +39,10 @@ EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH=*/20 * * ? * * EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH=0 */5 * ? * * # CRON interval for crawling DEAD connectors -EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH=0 */60 */4 ? * * +EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH=0 0 * ? * * # CRON interval for marking connectors as DEAD -EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS=0 0 12 ? * * +EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS=0 0 2 ? * * # Delete data offers / mark as dead after connector has been offline for: EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER=P5D From bd44a7cc8c848bab49454ad0eca1d481bbdbe1d6 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 12 Jul 2023 20:18:47 +0200 Subject: [PATCH 093/295] chore: prepare release (#195) --- CHANGELOG.md | 36 +++++-- connector/.env | 4 + .../api/BrokerServerResource.java | 5 +- .../brokerserver/BrokerServerExtension.java | 3 + .../BrokerServerExtensionContextBuilder.java | 7 +- .../BrokerServerResourceImpl.java | 5 +- .../services/api/CatalogApiService.java | 3 +- .../services/config/AdminApiKeyValidator.java | 30 ++++++ .../services/config/BrokerServerSettings.java | 1 + .../config/BrokerServerSettingsFactory.java | 4 + .../edc/ext/brokerserver/TestUtils.java | 5 +- .../services/api/AddConnectorsApiTest.java | 102 ++++++++++++++++++ .../services/api/CatalogApiTest.java | 18 ++-- .../services/api/ConnectorApiTest.java | 6 +- .../services/api/DataOfferDetailApiTest.java | 4 +- 15 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5568bdfe4..077a240bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,24 +13,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major -- Broker Server API now generates into it's own Broker Server Client Typescript Library. +#### Minor + +#### Patch + +### Deployment Migration Notes + +## [v1.0.0] - 2023-07-12 + +### Overview + +Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be added at runtime + +### Detailed Changes + +#### Major + +- Broker Server API now generates into its own Broker Server Client Typescript Library. #### Minor - Broker Server API is now part of this repository. - Dead Connectors are now deleted periodically. +- Connector Online Status is now visualized. #### Patch ### Deployment Migration Notes - -1. Removed **optional** configuration properties: +1. Added new **required** configuration properties: ```yaml - # (Removed) CRON interval for crawling connectors - EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "0 */5 * ? * *" + # Broker Server Admin Api Key (required) + # This is a stopgap until we have IAM + EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey ``` - -2. There are new **optional** configuration properties: +2. Added new **optional** configuration properties: ```yaml # CRON interval for crawling ONLINE connectors EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH: "*/20 * * ? * *" # every 20s @@ -50,6 +66,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Hide data offers after connector has been offline for: EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "P1D" ``` +3. Removed **optional** configuration properties: + ```yaml + # (Removed) CRON interval for crawling connectors + EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "0 */5 * ? * *" + ``` + ## [v0.1.0] Broker MvP Release - 2023-06-23 diff --git a/connector/.env b/connector/.env index b52a92f98..0afa888d7 100644 --- a/connector/.env +++ b/connector/.env @@ -16,6 +16,10 @@ MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url MY_EDC_JDBC_USER=missing-postgresql-user MY_EDC_JDBC_PASSWORD=missing-postgresql-password +# Broker Server Admin Api Key (required) +# This is a stopgap until we have IAM +EDC_BROKER_SERVER_ADMIN_API_KEY=DefaultBrokerServerAdminApiKey + # Database Connection Pool Size EDC_BROKER_SERVER_DB_CONNECTION_POOL_SIZE=30 diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index 851da1a93..2f9275e43 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -29,6 +29,7 @@ import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.util.List; @@ -67,7 +68,7 @@ public interface BrokerServerResource { @PUT @Path("connectors") - @Consumes + @Consumes(MediaType.APPLICATION_JSON) @Operation(description = "Add unknown Connectors to the Broker Server") - void addConnectors(List endpoints); + void addConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 49f2b5d16..65c61f688 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -29,6 +29,9 @@ public class BrokerServerExtension implements ServiceExtension { public static final String EXTENSION_NAME = "BrokerServerExtension"; + @Setting + public static final String ADMIN_API_KEY = toEdcProp("EDC_BROKER_SERVER_ADMIN_API_KEY"); + @Setting public static final String KNOWN_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_CONNECTORS"); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 40511d100..a5d882e93 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -43,6 +43,7 @@ import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterAttributeDefinitionService; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; +import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettingsFactory; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; @@ -98,6 +99,7 @@ public static BrokerServerExtensionContext buildContext( ) { var brokerServerSettingsFactory = new BrokerServerSettingsFactory(config, monitor); var brokerServerSettings = brokerServerSettingsFactory.buildBrokerServerSettings(); + var adminApiKeyValidator = new AdminApiKeyValidator(brokerServerSettings); // Dao var dataOfferQueries = new DataOfferQueries(); @@ -214,19 +216,18 @@ public static BrokerServerExtensionContext buildContext( connectorService, paginationMetadataUtils ); - var dataOfferDetailApiService = new DataOfferDetailApiService( dataOfferDetailPageQueryService, viewCountLogger, policyDtoBuilder, assetPropertyParser ); - var brokerServerResource = new BrokerServerResourceImpl( dslContextFactory, connectorApiService, catalogApiService, - dataOfferDetailApiService + dataOfferDetailApiService, + adminApiKeyValidator ); return new BrokerServerExtensionContext( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index d22d1c756..2f32ecc37 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -27,6 +27,7 @@ import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; +import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; import lombok.RequiredArgsConstructor; import java.util.List; @@ -41,6 +42,7 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final ConnectorApiService connectorApiService; private final CatalogApiService catalogApiService; private final DataOfferDetailApiService dataOfferDetailApiService; + private final AdminApiKeyValidator adminApiKeyValidator; @Override public CatalogPageResult catalogPage(CatalogPageQuery query) { @@ -63,7 +65,8 @@ public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery qu } @Override - public void addConnectors(List endpoints) { + public void addConnectors(List endpoints, String adminApiKey) { + adminApiKeyValidator.validateAdminApiKey(adminApiKey); dslContextFactory.transaction(dsl -> connectorApiService.addConnectors(dsl, endpoints)); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index e7d986578..0a08c44e0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -124,7 +124,8 @@ private static List buildAvailableSortings() { return Stream.of( CatalogPageSortingType.MOST_RECENT, CatalogPageSortingType.TITLE, - CatalogPageSortingType.ORIGINATOR + CatalogPageSortingType.ORIGINATOR, + CatalogPageSortingType.VIEW_COUNT ).map(it -> new CatalogPageSortingItem(it, it.getTitle())).toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java new file mode 100644 index 000000000..e5f9571b9 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.config; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AdminApiKeyValidator { + private final BrokerServerSettings brokerServerSettings; + + public void validateAdminApiKey(String adminApiKey) { + if (!brokerServerSettings.getAdminApiKey().equals(adminApiKey)) { + throw new WebApplicationException("Invalid admin API key", Response.Status.UNAUTHORIZED); + } + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java index beec07b01..5873d04a0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java @@ -22,6 +22,7 @@ @Value @Builder public class BrokerServerSettings { + String adminApiKey; Duration hideOfflineDataOffersAfter; int catalogPagePageSize; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java index bbc6486ed..6a83765de 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -18,6 +18,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; @@ -31,6 +32,8 @@ public class BrokerServerSettingsFactory { private final Monitor monitor; public BrokerServerSettings buildBrokerServerSettings() { + var adminApiKey = Validate.notBlank(config.getString(BrokerServerExtension.ADMIN_API_KEY), + "Need to configure %s.".formatted(BrokerServerExtension.ADMIN_API_KEY)); var hideOfflineDataOffersAfter = getDuration(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER, null); var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); var dataSpaceConfig = buildDataSpaceConfig(config); @@ -38,6 +41,7 @@ public BrokerServerSettings buildBrokerServerSettings() { var killOfflineConnectorsAfter = getDuration(BrokerServerExtension.KILL_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); return BrokerServerSettings.builder() + .adminApiKey(adminApiKey) .hideOfflineDataOffersAfter(hideOfflineDataOffersAfter) .catalogPagePageSize(catalogPagePageSize) .dataSpaceConfig(dataSpaceConfig) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 1f5046e77..655a084cb 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -35,6 +35,8 @@ public class TestUtils { public static final String MANAGEMENT_API_KEY = "123456"; public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + DATA_PATH; + public static final String ADMIN_API_KEY = "123456"; + public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH + "/data"; @@ -63,6 +65,7 @@ public static Map createConfiguration( config.put(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, "true"); config.put(PostgresFlywayExtension.FLYWAY_CLEAN, "true"); config.put(BrokerServerExtension.NUM_THREADS, "0"); + config.put(BrokerServerExtension.ADMIN_API_KEY, ADMIN_API_KEY); config.putAll(getCoreEdcJdbcConfig(testDatabase)); config.putAll(additionalConfigProperties); return config; @@ -85,7 +88,7 @@ private static Map getCoreEdcJdbcConfig(TestDatabase testDatabas return config; } - public static BrokerServerClient edcClient() { + public static BrokerServerClient brokerServerClient() { return BrokerServerClient.builder() .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) .managementApiKey(TestUtils.MANAGEMENT_API_KEY) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java new file mode 100644 index 000000000..5195ff3bb --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.BrokerServerExtension; +import de.sovity.edc.ext.brokerserver.client.gen.ApiException; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ApiTest +@ExtendWith(EdcExtension.class) +class AddConnectorsApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( + BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", + BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", + BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" + ))); + } + + @Test + void testAddAndMerge() { + TEST_DATABASE.testTransaction(dsl -> { + var client = brokerServerClient(); + + client.brokerServerApi().addConnectors(ADMIN_API_KEY, List.of()); + + client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( + null, + "", + " ", + "\t", + "http://a", + "http://b" + )); + + assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) + .extracting(ConnectorListEntry::getEndpoint) + .containsExactlyInAnyOrder("http://a", "http://b"); + + client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( + "http://b", + " http://b\r\n", + "http://c" + )); + + assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) + .extracting(ConnectorListEntry::getEndpoint) + .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); + + }); + } + + @Test + void testWrongApiKey() { + TEST_DATABASE.testTransaction(dsl -> { + var client = brokerServerClient(); + + assertThatThrownBy(() -> client.brokerServerApi().addConnectors("wrong-api-key", List.of())) + .isInstanceOf(ApiException.class) + .satisfies(ex -> { + var apiException = (ApiException) ex; + assertThat(apiException.getCode()).isEqualTo(401); + }); + }); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 8a39af447..cc9bbc3b9 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -47,7 +47,7 @@ import java.util.stream.IntStream; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -88,7 +88,7 @@ void testDataSpace_two_dataspaces_filter_for_one() { new CnfFilterValueAttribute("dataSpace", List.of("Example1")) ))); - var result = edcClient().brokerServerApi().catalogPage(query); + var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).hasSize(1); var dataOfferResult = result.getDataOffers().get(0); @@ -127,7 +127,7 @@ void test_available_filter_values_to_filter_by() { ), "http://my-connector3/ids/data"); // Dataspace: Example2 // get all available filter values - var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); // assert that the filter values are correct var dataSpace = getAvailableFilter(result, "dataSpace"); @@ -152,7 +152,7 @@ void testDataOfferDetails() { ), "http://my-connector/ids/data"); - var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); assertThat(result.getDataOffers()).hasSize(1); var dataOfferResult = result.getDataOffers().get(0); @@ -184,7 +184,7 @@ void testEmptyConnector() { createConnector(dsl, today, "http://my-connector/ids/data"); // act - var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); // assert assertThat(result.getDataOffers()).isEmpty(); @@ -227,7 +227,7 @@ void testAvailableFilters_noFilter() { ), "http://my-connector/ids/data"); - var result = edcClient().brokerServerApi().catalogPage(new CatalogPageQuery()); + var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); assertThat(result.getAvailableFilters().getFields()) .extracting(CnfFilterAttribute::getId) @@ -297,7 +297,7 @@ void testAvailableFilters_withFilter() { new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) ))); - var result = edcClient().brokerServerApi().catalogPage(query); + var result = brokerServerClient().brokerServerApi().catalogPage(query); var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); assertThat(dataCategory.getId()).isEqualTo(AssetProperty.DATA_CATEGORY); @@ -332,7 +332,7 @@ void testPagination_firstPage() { query.setSearchQuery("my-asset"); query.setSorting(CatalogPageQuery.SortingEnum.TITLE); - var result = edcClient().brokerServerApi().catalogPage(query); + var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) .isEqualTo(IntStream.range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); @@ -364,7 +364,7 @@ void testPagination_secondPage() { query.setPageOneBased(2); query.setSorting(CatalogPageQuery.SortingEnum.TITLE); - var result = edcClient().brokerServerApi().catalogPage(query); + var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) .isEqualTo(IntStream.range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index a57735a85..1436ffd18 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -42,7 +42,7 @@ import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static groovy.json.JsonOutput.toJson; import static org.assertj.core.api.Assertions.assertThat; @@ -71,7 +71,7 @@ void testQueryConnectors() { AssetProperty.ASSET_NAME, "My Asset 1" ), "http://my-connector/ids/data"); - var result = edcClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); + var result = brokerServerClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); assertThat(result.getConnectors()).hasSize(1); var connector = result.getConnectors().get(0); @@ -96,7 +96,7 @@ void testQueryConnectorDetails() { AssetProperty.ASSET_NAME, "My Asset 1" ), "http://my-connector/ids/data"); - var connector = edcClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("http://my-connector/ids/data")); + var connector = brokerServerClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("http://my-connector/ids/data")); assertThat(connector.getId()).isEqualTo("http://my-connector/ids/data"); assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index aa1cd06f2..260e4a82d 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -39,7 +39,7 @@ import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static de.sovity.edc.ext.brokerserver.TestUtils.edcClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static groovy.json.JsonOutput.toJson; import static org.assertj.core.api.Assertions.assertThat; @@ -87,7 +87,7 @@ void testQueryDataOfferDetails() { AssetProperty.ASSET_NAME, "My Asset 1" ), "http://my-connector/ids/data"); - var actual = edcClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("http://my-connector/ids/data", "urn:artifact:my-asset-1")); + var actual = brokerServerClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("http://my-connector/ids/data", "urn:artifact:my-asset-1")); assertThat(actual.getAssetId()).isEqualTo("urn:artifact:my-asset-1"); assertThat(actual.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); assertThat(actual.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); From 631df3ff5063fb8f667e8079393d16a2e7db11d2 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 12 Jul 2023 20:23:33 +0200 Subject: [PATCH 094/295] chore: prepare release (#197) --- .env | 6 +++--- CHANGELOG.md | 6 ++++++ gradle.properties | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5e0e82601..7cc9ea953 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.0 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.0.1 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 diff --git a/CHANGELOG.md b/CHANGELOG.md index 077a240bc..f5cb14b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,12 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde # (Removed) CRON interval for crawling connectors EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "0 */5 * ? * *" ``` + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` +- Sovity EDC CE: [`4.0.1`](https://github.com/sovity/edc-extensions/tree/v4.0.1/connector) ## [v0.1.0] Broker MvP Release - 2023-06-23 diff --git a/gradle.properties b/gradle.properties index 434b66b96..733ac7973 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,8 +2,8 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT -# Sovity EDC Extensions (for policy always true) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +# Sovity EDC Extensions (for common api model) +sovityEdcExtensionsVersion=4.0.1 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 834cb759b7bdd2c3c9f4ef0699a91a352647d102 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 12 Jul 2023 20:42:20 +0200 Subject: [PATCH 095/295] docs: dynamic adding of connector endpoints (#198) --- CHANGELOG.md | 9 +++++++++ README.md | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5cb14b72..a4da29322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,15 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde # (Removed) CRON interval for crawling connectors EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "0 */5 * ? * *" ``` +4. Connectors can now be dynamically added at runtime by using the following endpoint: + ```shell script + # Response should be 204 No Content + curl --request PUT \ + --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: ApiKeyDefaultValue' \ + --data '["https://some-new-connector/api/v1/ids/data", "https://some-other-new-connector/api/v1/ids/data"]' + ``` #### Compatible Versions diff --git a/README.md b/README.md index a34c1d885..86cf12ebe 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,9 @@ EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 # Required: Management API Key EDC_API_AUTH_KEY: "ApiKeyDefaultValue" + +# Required: Admin Api Key +EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey ``` All pre-configured config values for either the broker server or the underlying EDC can be found @@ -149,6 +152,19 @@ EDC_UI_DATA_MANAGEMENT_API_URL: https://my-broker.com/backend/api/v1/management EDC_API_AUTH_KEY: "ApiKeyDefaultValue" ``` +#### Adding Connectors at runtime + +Connectors can be dynamically added at runtime by using the following endpoint: + +```shell script +# Response should be 204 No Content +curl --request PUT \ + --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: ApiKeyDefaultValue' \ + --data '["https://some-new-connector/api/v1/ids/data", "https://some-other-new-connector/api/v1/ids/data"]' +``` +

(back to top)

## License From 4cb334e32b960c913e4c7d11e92e4481abc4a2a6 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 12 Jul 2023 20:44:15 +0200 Subject: [PATCH 096/295] chore: update release title --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4da29322..cb90506e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deployment Migration Notes -## [v1.0.0] - 2023-07-12 +## [v1.0.0] Broker MvP Bugfix / Feature Release - 2023-07-12 ### Overview From cfd96c5d7d2f2807b5531035028c2d1cfb578ab9 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 12 Jul 2023 20:48:09 +0200 Subject: [PATCH 097/295] chore: post release cleanup (#199) --- .env | 6 +++--- gradle.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 7cc9ea953..5e0e82601 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.0 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.0.1 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/gradle.properties b/gradle.properties index 733ac7973..0b716bf9c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=4.0.1 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From bcde51c6c9d7f961b85d8af07b36685145093f7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 07:44:20 +0200 Subject: [PATCH 098/295] chore(deps): bump org.flywaydb.flyway from 9.20.0 to 9.20.1 (#194) Bumps org.flywaydb.flyway from 9.20.0 to 9.20.1. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index b6e64bbf8..a584aafab 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -27,7 +27,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.20.0" + id("org.flywaydb.flyway") version "9.20.1" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From a24351b47177404288e440debb5dbb3827406596 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 13 Jul 2023 08:39:58 +0200 Subject: [PATCH 099/295] fix: Dockerfile healthcheck --- connector/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector/Dockerfile b/connector/Dockerfile index b353bd390..3549f3e74 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -37,7 +37,7 @@ COPY --from=build /home/gradle/project/connector/build/libs/app.jar /app COPY ./connector/src/main/resources/logging.properties /app # health status is determined by the availability of the /health endpoint -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "X-Api-Key: $EDC_API_AUTH_KEY" --fail http://localhost:11002/api/v1/management/check/health +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "X-Api-Key: $EDC_API_AUTH_KEY" --fail http://localhost:11002/backend/api/v1/management/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. # ARG can not be used in ENTRYPOINT so storing values in ENV variables From 68eb987f130af0ae2ec0af9708bfa7bf6f1072eb Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 13 Jul 2023 09:32:34 +0200 Subject: [PATCH 100/295] 2023 07 13 redo release (#201) * chore: prepare release * Revert "chore: post release cleanup (#199)" This reverts commit cfd96c5d7d2f2807b5531035028c2d1cfb578ab9. * chore: prepare release --- .env | 6 +++--- CHANGELOG.md | 11 ++++++++--- gradle.properties | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 5e0e82601..df8c628d3 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.1 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.0.1 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 diff --git a/CHANGELOG.md b/CHANGELOG.md index cb90506e4..d453f8f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deployment Migration Notes -## [v1.0.0] Broker MvP Bugfix / Feature Release - 2023-07-12 +## [v1.0.1] Broker MvP Bugfix / Feature Release - 2023-07-12 ### Overview -Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be added at runtime +Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be added at runtime. ### Detailed Changes @@ -39,6 +39,8 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde #### Patch +- Fixed Backend Docker Healthcheck + ### Deployment Migration Notes 1. Added new **required** configuration properties: ```yaml @@ -83,10 +85,13 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde #### Compatible Versions -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.0` +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.1` - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` - Sovity EDC CE: [`4.0.1`](https://github.com/sovity/edc-extensions/tree/v4.0.1/connector) +## [v1.0.0] + +Release was deleted in favor of above release. There was a bug, and we just decided to re-do the release. ## [v0.1.0] Broker MvP Release - 2023-06-23 diff --git a/gradle.properties b/gradle.properties index 0b716bf9c..733ac7973 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=4.0.1 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 45ca47ddffc4e6646f315340c4655f332a14c353 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 13 Jul 2023 09:35:31 +0200 Subject: [PATCH 101/295] chore: post release cleanup (#202) --- .env | 6 +++--- gradle.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env b/.env index df8c628d3..5e0e82601 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.1 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.0.1 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/gradle.properties b/gradle.properties index 733ac7973..0b716bf9c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=4.0.1 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From dae907479fd50b6a81225386679395928a05844f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jul 2023 08:10:25 +0200 Subject: [PATCH 102/295] chore(deps): bump org.flywaydb.flyway from 9.20.1 to 9.21.0 (#211) Bumps org.flywaydb.flyway from 9.20.1 to 9.21.0. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index a584aafab..9cb043f7c 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -27,7 +27,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.20.1" + id("org.flywaydb.flyway") version "9.21.0" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From 88ba5d990f068c65e352d279cfac099d2d42f316 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 07:57:37 +0200 Subject: [PATCH 103/295] chore(deps): bump org.junit.jupiter:junit-jupiter-api (#212) Bumps [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit5) from 5.9.3 to 5.10.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle.kts | 2 +- extensions/broker-server/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6bcd36515..36b3df55b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } dependencies { - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") } diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index cab342923..01711e60e 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { testImplementation("org.testcontainers:testcontainers:${testcontainersVersion}") testImplementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testImplementation("org.skyscreamer:jsonassert:1.5.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") From 1fc827a674bf24cbd7697548fd45c6a86dd29999 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:03:27 +0200 Subject: [PATCH 104/295] chore(deps): bump org.junit.jupiter:junit-jupiter-engine (#213) Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.9.3 to 5.10.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle.kts | 2 +- extensions/broker-server/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 36b3df55b..bd4c4bbbe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") } val downloadArtifact: Configuration by configurations.creating { diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 01711e60e..4eeeabab2 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testImplementation("org.skyscreamer:jsonassert:1.5.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") implementation("org.quartz-scheduler:quartz:2.3.2") } From 2e72795acda9c4b2dccaa56307143cc1664f1c74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:45:20 +0000 Subject: [PATCH 105/295] chore(deps): bump org.flywaydb.flyway from 9.21.0 to 9.21.1 (#219) Bumps org.flywaydb.flyway from 9.21.0 to 9.21.1. --- updated-dependencies: - dependency-name: org.flywaydb.flyway dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 9cb043f7c..8082f4e9c 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -27,7 +27,7 @@ buildscript { } plugins { - id("org.flywaydb.flyway") version "9.21.0" + id("org.flywaydb.flyway") version "9.21.1" id("nu.studer.jooq") version "7.1.1" `java-library` `maven-publish` From 934c20fd50f6c754e4b4116c8271d7e207e31cc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 08:18:46 +0200 Subject: [PATCH 106/295] chore(deps): bump org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0 (#220) Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0. --- updated-dependencies: - dependency-name: org.apache.commons:commons-lang3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- extensions/broker-server-api/client/build.gradle.kts | 2 +- .../broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- extensions/broker-server/build.gradle.kts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index c5adc345e..4bcffe07b 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -21,7 +21,7 @@ dependencies { api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") api("jakarta.servlet:jakarta.servlet-api:5.0.0") - implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") @@ -29,7 +29,7 @@ dependencies { implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("org.apache.commons:commons-lang3:3.13.0") } val openapiFileDir = "${project.buildDir}/swagger" diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index a78dfb2f9..c26b0cad6 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { implementation("com.google.code.gson:gson:2.10.1") implementation("io.gsonfire:gson-fire:1.8.5") implementation("org.openapitools:jackson-databind-nullable:0.2.6") - implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.annotation:jakarta.annotation-api:1.3.5") // Lombok diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 8082f4e9c..88c1941a2 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -43,7 +43,7 @@ dependencies { annotationProcessor("org.projectlombok:lombok:1.18.28") compileOnly("org.projectlombok:lombok:1.18.28") - implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("org.apache.commons:commons-lang3:3.13.0") implementation("${edcGroup}:core-spi:${edcVersion}") implementation("${edcGroup}:sql-core:${edcVersion}") diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 4eeeabab2..20ea20356 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -20,7 +20,7 @@ configurations.all { dependencies { annotationProcessor("org.projectlombok:lombok:1.18.28") compileOnly("org.projectlombok:lombok:1.18.28") - implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("org.apache.commons:commons-lang3:3.13.0") implementation("${edcGroup}:control-plane-spi:${edcVersion}") implementation("${edcGroup}:management-api-configuration:${edcVersion}") From 83a3a2f73894ed02846ae7445febef25bd723d59 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:55:00 +0200 Subject: [PATCH 107/295] fix: logging into db (#224) Co-authored-by: Richard Treier --- .../BrokerServerExtensionContextBuilder.java | 5 +- .../services/config/BrokerServerSettings.java | 3 + .../config/BrokerServerSettingsFactory.java | 4 ++ .../services/logging/BrokerEventLogger.java | 17 +++-- .../ConnectorUpdateSuccessWriter.java | 2 +- .../offers/DataOfferLimitsEnforcer.java | 28 ++++---- .../logging/BrokerEventLoggerTest.java | 11 +++- .../offers/DataOfferLimitsEnforcerTest.java | 64 +++++++++---------- 8 files changed, 76 insertions(+), 58 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index a5d882e93..b69648f4a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -122,7 +122,8 @@ public static BrokerServerExtensionContext buildContext( brokerServerSettings ); var connectorPageQueryService = new ConnectorPageQueryService(); - var dataOfferDetailPageQueryService = new DataOfferDetailPageQueryService(catalogQueryContractOfferFetcher, brokerServerSettings); + var dataOfferDetailPageQueryService = new DataOfferDetailPageQueryService( + catalogQueryContractOfferFetcher, brokerServerSettings); // Services @@ -132,7 +133,7 @@ public static BrokerServerExtensionContext buildContext( var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); var dataOfferRecordUpdater = new DataOfferRecordUpdater(); var dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); - var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(config, brokerEventLogger); + var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(brokerServerSettings, brokerEventLogger); var dataOfferPatchBuilder = new DataOfferPatchBuilder( dataOfferContractOfferQueries, dataOfferQueries, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java index 5873d04a0..cc4083105 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java @@ -32,4 +32,7 @@ public class BrokerServerSettings { int numThreads; Duration killOfflineConnectorsAfter; + + int maxDataOffersPerConnector; + int maxContractOffersPerDataOffer; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java index 6a83765de..a09814341 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java @@ -39,6 +39,8 @@ public BrokerServerSettings buildBrokerServerSettings() { var dataSpaceConfig = buildDataSpaceConfig(config); var numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); var killOfflineConnectorsAfter = getDuration(BrokerServerExtension.KILL_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); + var maxDataOffers = config.getInteger(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, -1); + var maxContractOffers = config.getInteger(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER, -1); return BrokerServerSettings.builder() .adminApiKey(adminApiKey) @@ -47,6 +49,8 @@ public BrokerServerSettings buildBrokerServerSettings() { .dataSpaceConfig(dataSpaceConfig) .numThreads(numThreads) .killOfflineConnectorsAfter(killOfflineConnectorsAfter) + .maxDataOffersPerConnector(maxDataOffers) + .maxContractOffersPerDataOffer(maxContractOffers) .build(); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index f97c48bab..ed88c2a8f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -17,7 +17,6 @@ import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.BrokerEventLogRecord; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -62,8 +61,8 @@ public void logConnectorOnline(DSLContext dsl, String connectorEndpoint) { logEntry.insert(); } - public void logConnectorUpdateDataOfferLimitExceeded(Integer maxDataOffersPerConnector, String endpoint) { - var logEntry = new BrokerEventLogRecord(); + public void logConnectorUpdateDataOfferLimitExceeded(DSLContext dsl, Integer maxDataOffersPerConnector, String endpoint) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); @@ -72,8 +71,8 @@ public void logConnectorUpdateDataOfferLimitExceeded(Integer maxDataOffersPerCon logEntry.insert(); } - public void logConnectorUpdateDataOfferLimitOk(String endpoint) { - var logEntry = new BrokerEventLogRecord(); + public void logConnectorUpdateDataOfferLimitOk(DSLContext dsl, String endpoint) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_OK); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); @@ -82,8 +81,8 @@ public void logConnectorUpdateDataOfferLimitOk(String endpoint) { logEntry.insert(); } - public void logConnectorUpdateContractOfferLimitExceeded(Integer maxContractOffersPerConnector, String endpoint) { - var logEntry = new BrokerEventLogRecord(); + public void logConnectorUpdateContractOfferLimitExceeded(DSLContext dsl, Integer maxContractOffersPerConnector, String endpoint) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); @@ -92,8 +91,8 @@ public void logConnectorUpdateContractOfferLimitExceeded(Integer maxContractOffe logEntry.insert(); } - public void logConnectorUpdateContractOfferLimitOk(String endpoint) { - var logEntry = new BrokerEventLogRecord(); + public void logConnectorUpdateContractOfferLimitOk(DSLContext dsl, String endpoint) { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_OK); logEntry.setEventStatus(BrokerEventStatus.OK); logEntry.setConnectorEndpoint(endpoint); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 83c61889b..dc2c3b6a9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -42,7 +42,7 @@ public void handleConnectorOnline( // Limit data offers and log limitation if necessary var limitedDataOffers = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, limitedDataOffers); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, limitedDataOffers); // Log Status Change and set status to online if necessary if (connector.getOnlineStatus() != ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java index b9ec5efb4..b4d067881 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java @@ -14,20 +14,22 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.system.configuration.Config; +import org.jooq.DSLContext; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; @RequiredArgsConstructor public class DataOfferLimitsEnforcer { - private final Config config; + private final BrokerServerSettings brokerServerSettings; private final BrokerEventLogger brokerEventLogger; public record DataOfferLimitsEnforced( @@ -39,9 +41,9 @@ public record DataOfferLimitsEnforced( public DataOfferLimitsEnforced enforceLimits(Collection dataOffers) { // Get limits from config - var maxDataOffers = config.getInteger(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, -1); - var maxContractOffers = config.getInteger(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER, -1); - var offerList = dataOffers.stream().toList(); + var maxDataOffers = brokerServerSettings.getMaxDataOffersPerConnector(); + var maxContractOffers = brokerServerSettings.getMaxContractOffersPerDataOffer(); + List offerList = new ArrayList<>(dataOffers); // No limits set if (maxDataOffers == -1 && maxContractOffers == -1) { @@ -68,22 +70,26 @@ public DataOfferLimitsEnforced enforceLimits(Collection dataOf return new DataOfferLimitsEnforced(offerList, dataOfferLimitsExceeded, contractOfferLimitsExceeded); } - public void logEnforcedLimitsIfChanged(ConnectorRecord connector, DataOfferLimitsEnforced enforcedLimits) { + public void logEnforcedLimitsIfChanged(DSLContext dsl, ConnectorRecord connector, DataOfferLimitsEnforced enforcedLimits) { + String endpoint = connector.getEndpoint(); + // DataOffer if (enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.OK) { - brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + var maxDataOffers = brokerServerSettings.getMaxDataOffersPerConnector(); + brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, maxDataOffers, endpoint); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.EXCEEDED); } else if (!enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.EXCEEDED) { - brokerEventLogger.logConnectorUpdateDataOfferLimitOk(connector.getEndpoint()); + brokerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, endpoint); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); } // ContractOffer if (enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.OK) { - brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(enforcedLimits.abbreviatedDataOffers.size(), connector.getEndpoint()); + var maxContractOffers = brokerServerSettings.getMaxContractOffersPerDataOffer(); + brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(dsl, maxContractOffers, endpoint); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.EXCEEDED); } else if (!enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.EXCEEDED) { - brokerEventLogger.logConnectorUpdateContractOfferLimitOk(connector.getEndpoint()); + brokerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, endpoint); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java index 1aca8a484..793ae7d2f 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java @@ -36,9 +36,14 @@ void testDataOfferWriter_allSortsOfUpdates() { var brokerEventLogger = new BrokerEventLogger(); // Test that insertions insert required fields and don't cause DB errors - brokerEventLogger.logConnectorUpdated(dsl, "https://example.com/ids/data", new ConnectorChangeTracker()); - brokerEventLogger.logConnectorOnline(dsl, "https://example.com/ids/data"); - brokerEventLogger.logConnectorOffline(dsl, "https://example.com/ids/data", new BrokerEventErrorMessage("Message", "Stacktrace")); + String endpoint = "https://example.com/ids/data"; + brokerEventLogger.logConnectorUpdated(dsl, endpoint, new ConnectorChangeTracker()); + brokerEventLogger.logConnectorOnline(dsl, endpoint); + brokerEventLogger.logConnectorOffline(dsl, endpoint, new BrokerEventErrorMessage("Message", "Stacktrace")); + brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(dsl, 10, endpoint); + brokerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, endpoint); + brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, 10, endpoint); + brokerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, endpoint); }); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java index 68d96465e..885ad6834 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java @@ -14,14 +14,14 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; -import org.eclipse.edc.spi.system.configuration.Config; +import org.jooq.DSLContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,22 +31,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class DataOfferLimitsEnforcerTest { DataOfferLimitsEnforcer dataOfferLimitsEnforcer; - Config config; + BrokerServerSettings settings; BrokerEventLogger brokerEventLogger; + DSLContext dsl; @BeforeEach void setup() { - config = mock(Config.class); + settings = mock(BrokerServerSettings.class); brokerEventLogger = mock(BrokerEventLogger.class); - dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(config, brokerEventLogger); + dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(settings, brokerEventLogger); + dsl = mock(DSLContext.class); } @Test @@ -54,8 +54,8 @@ void no_limit_and_two_dataofffers_and_contractoffer_should_not_limit() { // arrange int maxDataOffers = -1; int maxContractOffers = -1; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -78,14 +78,14 @@ void limit_zero_and_one_dataoffers_should_result_to_none() { // arrange int maxDataOffers = 0; int maxContractOffers = 0; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var dataOffers = List.of(new FetchedDataOffer()); // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - var actual = new ArrayList(enforcedLimits.abbreviatedDataOffers()); + var actual = new ArrayList<>(enforcedLimits.abbreviatedDataOffers()); var contractOffersLimitExceeded = enforcedLimits.contractOfferLimitsExceeded(); var dataOffersLimitExceeded = enforcedLimits.dataOfferLimitsExceeded(); @@ -100,8 +100,8 @@ void limit_one_and_two_dataoffers_should_result_to_one() { // arrange int maxDataOffers = 1; int maxContractOffers = 1; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -109,13 +109,13 @@ void limit_one_and_two_dataoffers_should_result_to_one() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - var actual = new ArrayList(enforcedLimits.abbreviatedDataOffers()); + var actual = new ArrayList<>(enforcedLimits.abbreviatedDataOffers()); var contractOffersLimitExceeded = enforcedLimits.contractOfferLimitsExceeded(); var dataOffersLimitExceeded = enforcedLimits.dataOfferLimitsExceeded(); // assert assertThat(actual).hasSize(1); - assertThat(((FetchedDataOffer) actual.get(0)).getContractOffers()).hasSize(1); + assertThat(actual.get(0).getContractOffers()).hasSize(1); assertTrue(contractOffersLimitExceeded); assertTrue(dataOffersLimitExceeded); } @@ -129,8 +129,8 @@ void verify_logConnectorUpdateDataOfferLimitExceeded() { int maxDataOffers = 1; int maxContractOffers = 1; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -138,10 +138,10 @@ void verify_logConnectorUpdateDataOfferLimitExceeded() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateDataOfferLimitExceeded(1, connector.getEndpoint()); + verify(brokerEventLogger).logConnectorUpdateDataOfferLimitExceeded(dsl, 1, connector.getEndpoint()); } @Test @@ -153,8 +153,8 @@ void verify_logConnectorUpdateDataOfferLimitOk() { int maxDataOffers = -1; int maxContractOffers = -1; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -162,10 +162,10 @@ void verify_logConnectorUpdateDataOfferLimitOk() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateDataOfferLimitOk(connector.getEndpoint()); + verify(brokerEventLogger).logConnectorUpdateDataOfferLimitOk(dsl, connector.getEndpoint()); } @Test @@ -177,8 +177,8 @@ void verify_logConnectorUpdateContractOfferLimitExceeded() { int maxDataOffers = 1; int maxContractOffers = 1; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -186,10 +186,10 @@ void verify_logConnectorUpdateContractOfferLimitExceeded() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateContractOfferLimitExceeded(1, connector.getEndpoint()); + verify(brokerEventLogger).logConnectorUpdateContractOfferLimitExceeded(dsl, 1, connector.getEndpoint()); } @Test @@ -201,8 +201,8 @@ void verify_logConnectorUpdateContractOfferLimitOk() { int maxDataOffers = -1; int maxContractOffers = -1; - when(config.getInteger(eq(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR), any())).thenReturn(maxDataOffers); - when(config.getInteger(eq(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER), any())).thenReturn(maxContractOffers); + when(settings.getMaxDataOffersPerConnector()).thenReturn(maxDataOffers); + when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); @@ -210,9 +210,9 @@ void verify_logConnectorUpdateContractOfferLimitOk() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateContractOfferLimitOk(connector.getEndpoint()); + verify(brokerEventLogger).logConnectorUpdateContractOfferLimitOk(dsl, connector.getEndpoint()); } } From 98ff9e3a76805f014317cbe083e0d83e629aba44 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 7 Aug 2023 11:56:13 +0200 Subject: [PATCH 108/295] chore: update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d453f8f61..6c2f313a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Patch +- Fixed an issue where connector crawling failed when data offer limits were exceeded. + ### Deployment Migration Notes ## [v1.0.1] Broker MvP Bugfix / Feature Release - 2023-07-12 From ed5222f6b78401aaba377941e5df8e82853b786b Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 8 Aug 2023 10:50:01 +0200 Subject: [PATCH 109/295] fix: data offers with capital letters are searchable again (#227) --- .../ext/brokerserver/dao/utils/LikeUtils.java | 10 +++--- .../services/api/CatalogApiTest.java | 36 +++++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java index 04fabd3db..c3823b04a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java @@ -30,16 +30,16 @@ public class LikeUtils { /** * Create LIKE condition value for "field contains word". * - * @param field field - * @param word word + * @param field field + * @param lowercaseWord word * @return "%escapedWord%" */ - public static Condition contains(Field field, String word) { - if (StringUtils.isBlank(word)) { + public static Condition contains(Field field, String lowercaseWord) { + if (StringUtils.isBlank(lowercaseWord)) { return DSL.trueCondition(); } - return field.like("%" + escape(word) + "%"); + return field.likeIgnoreCase("%" + escape(lowercaseWord) + "%"); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index cc9bbc3b9..d0ff78354 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -46,8 +46,8 @@ import java.util.Map; import java.util.stream.IntStream; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -165,11 +165,6 @@ void testDataOfferDetails() { AssetProperty.ASSET_NAME, "my-asset" )); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); - - // Key order of Json-String might differ, so we compare the JSON-Objects for similarity -// var actual = dataOfferResult.getContractOffers().get(0).getContractPolicy().getLegacyPolicy(); -// var expected = toJson(dummyPolicy()); -// assertEqualJson(expected, toJson(actual)); }); } @@ -268,6 +263,35 @@ void testAvailableFilters_noFilter() { }); } + + /** + * Regression Test against bug where asset names with capital letters were not hit by search. + *
+ * It was caused by search terms getting lower cased while the LIKE operation being case-sensitive. + */ + @Test + void testSearchCaseInsensitive() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today, "http://my-connector/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "123", + AssetProperty.ASSET_NAME, "Hello" + ), "http://my-connector/ids/data"); + + + // act + var query = new CatalogPageQuery(); + query.setSearchQuery("Hello"); + var result = brokerServerClient().brokerServerApi().catalogPage(query); + + // assert + assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly("123"); + }); + } + private CnfFilterAttribute getAvailableFilter(CatalogPageResult result, String filterId) { return result.getAvailableFilters().getFields().stream() .filter(it -> it.getId().equals(filterId)).findFirst() From d85656349a3b964dc3372cd5b77b663be9a3bbdf Mon Sep 17 00:00:00 2001 From: Sebastian Opriel Date: Thu, 10 Aug 2023 14:03:41 +0200 Subject: [PATCH 110/295] Update CHANGELOG.md (#228) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2f313a3..bd7b5fb25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Patch - Fixed an issue where connector crawling failed when data offer limits were exceeded. +- Fixed searching data offers with capital letters didn't work. ### Deployment Migration Notes From 4aa9553df1f4631cdf028e2782f295415942d8f0 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:25:35 +0200 Subject: [PATCH 111/295] chore: migrate Dockerfile to alpine (#222) --------- Co-authored-by: Richard Treier --- connector/Dockerfile | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/connector/Dockerfile b/connector/Dockerfile index 3549f3e74..26791c766 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:7.6.0-jdk17 AS build +FROM gradle:7-jdk17-alpine AS build ARG USERNAME ARG TOKEN @@ -19,17 +19,13 @@ COPY --chown=gradle:gradle . /home/gradle/project/ WORKDIR /home/gradle/project/ RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle build --no-daemon $BUILD_ARGS -# -buster is required to have apt available -FROM openjdk:17-slim-buster +FROM eclipse-temurin:17-jre-alpine # Optional JVM arguments, such as memory settings ARG JVM_ARGS="" -# Install curl, then delete apt indexes to save image space -RUN apt update \ - && apt install -y curl \ - && rm -rf /var/cache/apt/archives /var/lib/apt/lists \ - && touch /emtpy-properties-file.properties +# Install curl for healthcheck and create an empty properties file as migitation for a core EDC issue +RUN apk add --no-cache curl && touch /emtpy-properties-file.properties WORKDIR /app From de330362c798e6a70bc1bcd9ccc3da80278e9ce4 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:58:38 +0200 Subject: [PATCH 112/295] chore: prep release (#230) --------- Co-authored-by: Richard Treier --- .env | 6 +++--- CHANGELOG.md | 20 ++++++++++++++++++++ gradle.properties | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 5e0e82601..1e9a35181 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.2 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.1.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7b5fb25..9422e3d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,11 +17,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Patch +### Deployment Migration Notes + +## [v1.0.2] - 2023-08-10 + +### Overview + +Bugfix Release for the Broker MvP with MS8. + +### Detailed Changes + +#### Patch + - Fixed an issue where connector crawling failed when data offer limits were exceeded. - Fixed searching data offers with capital letters didn't work. ### Deployment Migration Notes +No configuration changes are required. + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.2` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` +- Sovity EDC CE: [`4.1.0`](https://github.com/sovity/edc-extensions/tree/v4.1.0/connector) + ## [v1.0.1] Broker MvP Bugfix / Feature Release - 2023-07-12 ### Overview diff --git a/gradle.properties b/gradle.properties index 0b716bf9c..00dfc1d2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=4.1.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From accf068a18b7bb3f1539aa4aa55dfc45f40de224 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:27:40 +0200 Subject: [PATCH 113/295] chore: post release cleanup (#231) --- .env | 6 +++--- gradle.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 1e9a35181..5e0e82601 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.2 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.1.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/gradle.properties b/gradle.properties index 00dfc1d2a..0b716bf9c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=4.1.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From c526bdb2f4fbaf42f753e9301ba138bc6dd28cfb Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:57:47 +0200 Subject: [PATCH 114/295] chore: release prep (redo release, fix dockerfile, #232) * Revert "chore: post release cleanup" This reverts commit 6e8da97b1f1b25c059bcf6f17e724568a8bf892e. * fix: dockerfile --- .env | 6 +++--- connector/Dockerfile | 5 +++-- gradle.properties | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 5e0e82601..1e9a35181 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.2 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.1.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 diff --git a/connector/Dockerfile b/connector/Dockerfile index 26791c766..822159b04 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -25,7 +25,8 @@ FROM eclipse-temurin:17-jre-alpine ARG JVM_ARGS="" # Install curl for healthcheck and create an empty properties file as migitation for a core EDC issue -RUN apk add --no-cache curl && touch /emtpy-properties-file.properties +RUN apk add --no-cache curl bash && touch /emtpy-properties-file.properties +SHELL ["/bin/bash", "-c"] WORKDIR /app @@ -44,4 +45,4 @@ COPY ./connector/.env /app/.env # Replaces ENV Var statements so they don't overwrite existing ENV Vars RUN sed -ri 's/^\s*(\S+)=(.*)$/\1=${\1:-"\2"}/' .env -ENTRYPOINT bash -c 'set -a && source /app/.env && set +a && exec java -Djava.util.logging.config.file=/app/logging.properties $JVM_ARGS -jar app.jar' +ENTRYPOINT set -a && source /app/.env && set +a && exec java -Djava.util.logging.config.file=/app/logging.properties $JVM_ARGS -jar app.jar diff --git a/gradle.properties b/gradle.properties index 0b716bf9c..00dfc1d2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=4.1.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 03a619eb4283b658ff4ab884191509d7f83f066d Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:02:31 +0200 Subject: [PATCH 115/295] chore: post release cleanup (#233) --- .env | 6 +++--- gradle.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 1e9a35181..5e0e82601 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.2 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.1.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/gradle.properties b/gradle.properties index 00dfc1d2a..0b716bf9c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=4.1.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From aa705b6390bab7ffd386970e9969723426b53a09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 08:05:11 +0200 Subject: [PATCH 116/295] chore(deps): bump org.jooq:jooq from 3.18.5 to 3.18.6 (#234) Bumps org.jooq:jooq from 3.18.5 to 3.18.6. --- updated-dependencies: - dependency-name: org.jooq:jooq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 88c1941a2..278e800b4 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -34,7 +34,7 @@ plugins { } dependencies { - api("org.jooq:jooq:3.18.5") + api("org.jooq:jooq:3.18.6") api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") jooqGenerator("org.postgresql:postgresql:42.6.0") From b5bcebf943d46f70ad4cbb3b1da5e044705d70cc Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 1 Sep 2023 11:59:13 +0200 Subject: [PATCH 117/295] fix: sorting by popularity (#244) --- .../CatalogQueryContractOfferFetcher.java | 3 +- .../dao/pages/catalog/CatalogQueryFields.java | 20 +- .../DataOfferDetailPageQueryService.java | 5 +- .../services/api/PolicyDtoBuilder.java | 12 + .../services/api/CatalogApiTest.java | 207 +++++++++++------- .../services/api/DataOfferDetailApiTest.java | 3 +- gradle.properties | 2 +- 7 files changed, 158 insertions(+), 94 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java index f4dd1f5a3..364c7683b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java @@ -33,7 +33,6 @@ public class CatalogQueryContractOfferFetcher { * @return {@link Field} of {@link ContractOfferRs}s */ public Field> getContractOffers(CatalogQueryFields fields) { - var c = fields.getConnectorTable(); var d = fields.getDataOfferTable(); var co = Tables.DATA_OFFER_CONTRACT_OFFER; @@ -43,7 +42,7 @@ public Field> getContractOffers(CatalogQueryFields fields) co.CREATED_AT, co.UPDATED_AT ).from(co).where( - co.CONNECTOR_ENDPOINT.eq(c.ENDPOINT), + co.CONNECTOR_ENDPOINT.eq(d.CONNECTOR_ENDPOINT), co.ASSET_ID.eq(d.ASSET_ID)).orderBy(co.CREATED_AT.desc() ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index 68ae36b20..7f255b146 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -24,6 +24,7 @@ import lombok.Getter; import lombok.experimental.FieldDefaults; import org.jooq.Field; +import org.jooq.Table; import org.jooq.impl.DSL; import java.time.OffsetDateTime; @@ -53,7 +54,12 @@ public class CatalogQueryFields { DataSpaceConfig dataSpaceConfig; - public CatalogQueryFields(Connector connectorTable, DataOffer dataOfferTable, DataOfferViewCount dataOfferViewCountTable, DataSpaceConfig dataSpaceConfig) { + public CatalogQueryFields( + Connector connectorTable, + DataOffer dataOfferTable, + DataOfferViewCount dataOfferViewCountTable, + DataSpaceConfig dataSpaceConfig + ) { this.connectorTable = connectorTable; this.dataOfferTable = dataOfferTable; this.dataOfferViewCountTable = dataOfferViewCountTable; @@ -94,18 +100,22 @@ public Field getAssetProperty(String name) { public CatalogQueryFields withSuffix(String additionalSuffix) { return new CatalogQueryFields( - connectorTable.as(connectorTable.getName() + "_" + additionalSuffix), - dataOfferTable.as(dataOfferTable.getName() + "_" + additionalSuffix), - dataOfferViewCountTable.as(dataOfferViewCountTable.getName() + "_" + additionalSuffix), + connectorTable.as(withSuffix(connectorTable, additionalSuffix)), + dataOfferTable.as(withSuffix(dataOfferTable, additionalSuffix)), + dataOfferViewCountTable.as(withSuffix(dataOfferViewCountTable, additionalSuffix)), dataSpaceConfig ); } + private String withSuffix(Table table, String additionalSuffix) { + return "%s_%s".formatted(table.getName(), additionalSuffix); + } + public Field getViewCount() { var subquery = DSL.select(DSL.count()) .from(dataOfferViewCountTable) .where(dataOfferViewCountTable.ASSET_ID.eq(dataOfferTable.ASSET_ID) - .and(dataOfferViewCountTable.CONNECTOR_ENDPOINT.eq(connectorTable.ENDPOINT))); + .and(dataOfferViewCountTable.CONNECTOR_ENDPOINT.eq(connectorTable.ENDPOINT))); return subquery.asField(); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java index 7ca8a7d7c..fcfd96e6a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java @@ -49,8 +49,9 @@ public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetI c.ENDPOINT.as("connectorEndpoint"), c.ONLINE_STATUS.as("connectorOnlineStatus"), fields.getViewCount().as("viewCount")) - .from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) - .where(d.ASSET_ID.eq(assetId).or(d.CONNECTOR_ENDPOINT.eq(endpoint))) + .from(d) + .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) + .where(d.ASSET_ID.eq(assetId).and(d.CONNECTOR_ENDPOINT.eq(endpoint))) .fetchOneInto(DataOfferDetailRs.class); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java index 21c10adba..5768ee970 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java @@ -14,10 +14,15 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.wrapper.api.common.model.ExpressionDto; +import de.sovity.edc.ext.wrapper.api.common.model.PermissionDto; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; + +import java.util.List; @RequiredArgsConstructor public class PolicyDtoBuilder { @@ -26,6 +31,13 @@ public class PolicyDtoBuilder { public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { var policyDto = new PolicyDto(); policyDto.setLegacyPolicy(policyJson); + policyDto.setPermission(extractPermissions(policyJson)); return policyDto; } + + @NotNull + private PermissionDto extractPermissions(String policyJson) { + // TODO + return new PermissionDto(new ExpressionDto(ExpressionDto.Type.AND, null, List.of(), null, null)); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index d0ff78354..006ec3e98 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -23,6 +23,8 @@ import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterItem; import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValue; import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValueAttribute; +import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageQuery; +import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; @@ -34,6 +36,7 @@ import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; import org.jooq.DSLContext; import org.jooq.JSONB; import org.junit.jupiter.api.BeforeEach; @@ -48,6 +51,7 @@ import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static java.util.stream.IntStream.*; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -60,9 +64,9 @@ class CatalogApiTest { @BeforeEach void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", - BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" + BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", + BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", + BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" ))); } @@ -75,17 +79,17 @@ void testDataSpace_two_dataspaces_filter_for_one() { createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: MDS createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector2/ids/data"); // Dataspace: Example1 var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataSpace", List.of("Example1")) + new CnfFilterValueAttribute("dataSpace", List.of("Example1")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); @@ -106,24 +110,24 @@ void test_available_filter_values_to_filter_by() { createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 createConnector(dsl, today, "http://my-connector3/ids/data"); // Dataspace: Example2 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "de" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "de" ), "http://my-connector/ids/data"); // Dataspace: MDS createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "en" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "en" ), "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset2", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" + AssetProperty.ASSET_ID, "urn:artifact:my-asset2", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" ), "http://my-connector2/ids/data"); // Dataspace: Example1 createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset3", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" + AssetProperty.ASSET_ID, "urn:artifact:my-asset3", + AssetProperty.ASSET_NAME, "my-asset", + AssetProperty.LANGUAGE, "fr" ), "http://my-connector3/ids/data"); // Dataspace: Example2 // get all available filter values @@ -132,9 +136,9 @@ void test_available_filter_values_to_filter_by() { // assert that the filter values are correct var dataSpace = getAvailableFilter(result, "dataSpace"); assertThat(dataSpace.getValues()).containsExactly( - new CnfFilterItem("Example1", "Example1"), - new CnfFilterItem("Example2", "Example2"), - new CnfFilterItem("MDS", "MDS") + new CnfFilterItem("Example1", "Example1"), + new CnfFilterItem("Example2", "Example2"), + new CnfFilterItem("MDS", "MDS") ); }); } @@ -147,8 +151,8 @@ void testDataOfferDetails() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" ), "http://my-connector/ids/data"); @@ -161,8 +165,8 @@ void testDataOfferDetails() { assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(CatalogDataOffer.ConnectorOnlineStatusEnum.ONLINE); assertThat(dataOfferResult.getAssetId()).isEqualTo("urn:artifact:my-asset"); assertThat(dataOfferResult.getProperties()).isEqualTo(Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" )); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); }); @@ -198,53 +202,53 @@ void testAvailableFilters_noFilter() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", - AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", + AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", + AssetProperty.DATA_CATEGORY, "my-category-1", + AssetProperty.TRANSPORT_MODE, "" ), "http://my-connector/ids/data"); var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getId) - .containsExactly( - "dataSpace", - AssetProperty.DATA_CATEGORY, - AssetProperty.DATA_SUBCATEGORY, - AssetProperty.DATA_MODEL, - AssetProperty.TRANSPORT_MODE, - AssetProperty.GEO_REFERENCE_METHOD - ); + .extracting(CnfFilterAttribute::getId) + .containsExactly( + "dataSpace", + AssetProperty.DATA_CATEGORY, + AssetProperty.DATA_SUBCATEGORY, + AssetProperty.DATA_MODEL, + AssetProperty.TRANSPORT_MODE, + AssetProperty.GEO_REFERENCE_METHOD + ); assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getTitle) - .containsExactly( - "Data Space", - "Data Category", - "Data Subcategory", - "Data Model", - "Transport Mode", - "Geo Reference Method" - ); + .extracting(CnfFilterAttribute::getTitle) + .containsExactly( + "Data Space", + "Data Category", + "Data Subcategory", + "Data Model", + "Transport Mode", + "Geo Reference Method" + ); var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); assertThat(dataCategory.getTitle()).isEqualTo("Data Category"); @@ -277,8 +281,8 @@ void testSearchCaseInsensitive() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "123", - AssetProperty.ASSET_NAME, "Hello" + AssetProperty.ASSET_ID, "123", + AssetProperty.ASSET_NAME, "Hello" ), "http://my-connector/ids/data"); @@ -294,8 +298,8 @@ void testSearchCaseInsensitive() { private CnfFilterAttribute getAvailableFilter(CatalogPageResult result, String filterId) { return result.getAvailableFilters().getFields().stream() - .filter(it -> it.getId().equals(filterId)).findFirst() - .orElseThrow(() -> new IllegalStateException("Filter not found")); + .filter(it -> it.getId().equals(filterId)).findFirst() + .orElseThrow(() -> new IllegalStateException("Filter not found")); } @Test @@ -306,19 +310,19 @@ void testAvailableFilters_withFilter() { createConnector(dsl, today, "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.DATA_SUBCATEGORY, "my-subcategory" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", + AssetProperty.DATA_CATEGORY, "my-category", + AssetProperty.DATA_SUBCATEGORY, "my-subcategory" ), "http://my-connector/ids/data"); createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_SUBCATEGORY, "my-other-subcategory" + AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", + AssetProperty.DATA_SUBCATEGORY, "my-other-subcategory" ), "http://my-connector/ids/data"); var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) + new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); @@ -344,11 +348,11 @@ void testPagination_firstPage() { var today = OffsetDateTime.now().withNano(0); createConnector(dsl, today, "http://my-connector/ids/data"); - IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) + range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) ), "http://my-connector/ids/data")); - IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) + range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) ), "http://my-connector/ids/data")); @@ -358,7 +362,7 @@ void testPagination_firstPage() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(IntStream.range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + .isEqualTo(range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(1); @@ -375,11 +379,11 @@ void testPagination_secondPage() { var today = OffsetDateTime.now().withNano(0); createConnector(dsl, today, "http://my-connector/ids/data"); - IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) + range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) ), "http://my-connector/ids/data")); - IntStream.range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) + range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) ), "http://my-connector/ids/data")); @@ -391,7 +395,7 @@ void testPagination_secondPage() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(IntStream.range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + .isEqualTo(range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(2); @@ -401,6 +405,34 @@ void testPagination_secondPage() { }); } + @Test + void testSortingByPopularity() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var endpoint = "http://my-connector/ids/data"; + createConnector(dsl, today, endpoint); + createDataOffer(dsl, today, Map.of(AssetProperty.ASSET_ID, "urn:artifact:asset-1"), endpoint); + createDataOffer(dsl, today, Map.of(AssetProperty.ASSET_ID, "urn:artifact:asset-2"), endpoint); + createDataOffer(dsl, today, Map.of(AssetProperty.ASSET_ID, "urn:artifact:asset-3"), endpoint); + + range(0, 3).forEach(i -> dataOfferDetails(endpoint, "urn:artifact:asset-1")); + range(0, 5).forEach(i -> dataOfferDetails(endpoint, "urn:artifact:asset-2")); + + + var query = new CatalogPageQuery(); + query.setSorting(CatalogPageQuery.SortingEnum.VIEW_COUNT); + + var result = brokerServerClient().brokerServerApi().catalogPage(query); + assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly( + "urn:artifact:asset-2", + "urn:artifact:asset-1", + "urn:artifact:asset-3" + ); + }); + } + private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); @@ -436,16 +468,25 @@ private void createConnector(DSLContext dsl, OffsetDateTime today, String connec private Policy dummyPolicy() { return Policy.Builder.newInstance() - .assignee("Example Assignee") - .build(); + .type(PolicyType.SET) + .build(); + } + + private DataOfferDetailPageResult dataOfferDetails(String endpoint, String assetId) { + var query = DataOfferDetailPageQuery.builder() + .connectorEndpoint(endpoint) + .assetId(assetId) + .build(); + return brokerServerClient().brokerServerApi().dataOfferDetailPage(query); } private String policyToJson(Policy policy) { return toJson(policy); } + @SneakyThrows private String toJson(Object o) { - return new ObjectMapper().valueToTree(o).toString(); + return new ObjectMapper().writeValueAsString(o); } @SneakyThrows diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index 260e4a82d..44e72ded3 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -38,6 +38,7 @@ import java.time.OffsetDateTime; import java.util.Map; +import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static groovy.json.JsonOutput.toJson; @@ -102,7 +103,7 @@ void testQueryDataOfferDetails() { assertThat(actual.getContractOffers()).hasSize(1); var contractOffer = actual.getContractOffers().get(0); assertThat(contractOffer.getContractOfferId()).isEqualTo("my-contract-offer-1"); - //assertEqualJson(contractOffer.getContractPolicy().getLegacyPolicy(), policyToJson(dummyPolicy())); + assertEqualJson(contractOffer.getContractPolicy().getLegacyPolicy(), policyToJson(dummyPolicy())); assertThat(contractOffer.getCreatedAt()).isEqualTo(today.minusDays(5)); assertThat(contractOffer.getUpdatedAt()).isEqualTo(today); assertThat(actual.getViewCount()).isEqualTo(2); diff --git a/gradle.properties b/gradle.properties index 0b716bf9c..00dfc1d2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=4.1.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 8761a9dfc0354921f4e5430b26a94434e2f0c606 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 1 Sep 2023 14:10:31 +0200 Subject: [PATCH 118/295] chore: prepare release (#246) --- .env | 6 +++--- CHANGELOG.md | 22 ++++++++++++++++++++++ gradle.properties | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 5e0e82601..9d153f1f7 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:latest -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.3 +EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.2.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9422e3d4f..18882cf8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deployment Migration Notes +## [v1.0.3] - 2023-09-01 + +### Overview + +Bugfix Release for the Broker MvP with MS8. + +### Detailed Changes + +#### Patch + +- Fixed sorting the catalog by popularity. + +### Deployment Migration Notes + +No configuration changes are required. + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.3` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` +- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) + ## [v1.0.2] - 2023-08-10 ### Overview diff --git a/gradle.properties b/gradle.properties index 00dfc1d2a..833a40846 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=4.1.0 +sovityEdcExtensionsVersion=4.2.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 9ca80784fdfa7dcaea3a8fc2e4164a6e67c5de9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:28:53 +0200 Subject: [PATCH 119/295] chore(deps): bump org.projectlombok:lombok from 1.18.28 to 1.18.30 (#259) Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.28 to 1.18.30. - [Release notes](https://github.com/projectlombok/lombok/releases) - [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown) - [Commits](https://github.com/projectlombok/lombok/compare/v1.18.28...v1.18.30) --- updated-dependencies: - dependency-name: org.projectlombok:lombok dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- extensions/broker-server-api/client/build.gradle.kts | 4 ++-- .../broker-server-postgres-flyway-jooq/build.gradle.kts | 4 ++-- extensions/broker-server/build.gradle.kts | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 4bcffe07b..f11954300 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -10,8 +10,8 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:1.18.28") - compileOnly("org.projectlombok:lombok:1.18.28") + annotationProcessor("org.projectlombok:lombok:1.18.30") + compileOnly("org.projectlombok:lombok:1.18.30") api("${sovityEdcGroup}:wrapper-common-api:${sovityEdcExtensionsVersion}") diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index c26b0cad6..ed2f50f7d 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -35,8 +35,8 @@ dependencies { implementation("jakarta.annotation:jakarta.annotation-api:1.3.5") // Lombok - compileOnly("org.projectlombok:lombok:1.18.28") - annotationProcessor("org.projectlombok:lombok:1.18.28") + compileOnly("org.projectlombok:lombok:1.18.30") + annotationProcessor("org.projectlombok:lombok:1.18.30") } tasks.getByName("test") { diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 278e800b4..c3f2a9cbc 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -41,8 +41,8 @@ dependencies { flywayMigration("org.postgresql:postgresql:42.6.0") implementation("com.zaxxer:HikariCP:5.0.1") - annotationProcessor("org.projectlombok:lombok:1.18.28") - compileOnly("org.projectlombok:lombok:1.18.28") + annotationProcessor("org.projectlombok:lombok:1.18.30") + compileOnly("org.projectlombok:lombok:1.18.30") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("${edcGroup}:core-spi:${edcVersion}") diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 20ea20356..aec9ba7b1 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -18,8 +18,8 @@ configurations.all { } dependencies { - annotationProcessor("org.projectlombok:lombok:1.18.28") - compileOnly("org.projectlombok:lombok:1.18.28") + annotationProcessor("org.projectlombok:lombok:1.18.30") + compileOnly("org.projectlombok:lombok:1.18.30") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("${edcGroup}:control-plane-spi:${edcVersion}") @@ -30,8 +30,8 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") - testAnnotationProcessor("org.projectlombok:lombok:1.18.28") - testCompileOnly("org.projectlombok:lombok:1.18.28") + testAnnotationProcessor("org.projectlombok:lombok:1.18.30") + testCompileOnly("org.projectlombok:lombok:1.18.30") testImplementation("org.assertj:assertj-core:${assertj}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("org.mockito:mockito-inline:${mockitoVersion}") From a4dd6c33feedc38925759d4438c51795d61af495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:30:40 +0200 Subject: [PATCH 120/295] chore(deps): bump docker/build-push-action from 4 to 5 (#256) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-and-publish-connector-images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish-connector-images.yml b/.github/workflows/build-and-publish-connector-images.yml index 794a7822d..76958808c 100644 --- a/.github/workflows/build-and-publish-connector-images.yml +++ b/.github/workflows/build-and-publish-connector-images.yml @@ -80,7 +80,7 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=raw,value=release,enable=${{ startsWith(github.ref, 'refs/tags/') }} - name: Build and push EDC image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: file: connector/Dockerfile context: . From 723372050e1ee11ade7692cefb7ae26df4f681c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:35:42 +0200 Subject: [PATCH 121/295] chore(deps): bump docker/login-action from 2 to 3 (#254) Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-and-publish-connector-images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish-connector-images.yml b/.github/workflows/build-and-publish-connector-images.yml index 76958808c..f0c71a01f 100644 --- a/.github/workflows/build-and-publish-connector-images.yml +++ b/.github/workflows/build-and-publish-connector-images.yml @@ -56,7 +56,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 9c3c2a14bea8f45c59f5c1d9f0a47806d6590be4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:40:21 +0200 Subject: [PATCH 122/295] chore(deps): bump actions/checkout from 3 to 4 (#247) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-and-publish-connector-images.yml | 2 +- .github/workflows/build-and-publish-ts-api-client.yml | 2 +- .github/workflows/code_analysis.yml | 6 +++--- .github/workflows/license_scan.yml | 2 +- .github/workflows/release_docs_zip.yml | 2 +- .github/workflows/secret_scan.yml | 2 +- .github/workflows/security_scan.yml | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-and-publish-connector-images.yml b/.github/workflows/build-and-publish-connector-images.yml index f0c71a01f..cfb17c1d9 100644 --- a/.github/workflows/build-and-publish-connector-images.yml +++ b/.github/workflows/build-and-publish-connector-images.yml @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v3 with: diff --git a/.github/workflows/build-and-publish-ts-api-client.yml b/.github/workflows/build-and-publish-ts-api-client.yml index 128fbaad6..37b80cfd3 100644 --- a/.github/workflows/build-and-publish-ts-api-client.yml +++ b/.github/workflows/build-and-publish-ts-api-client.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: FranzDiebold/github-env-vars-action@v2 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: 'temurin' diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml index 0e9cab29b..74afd292f 100644 --- a/.github/workflows/code_analysis.yml +++ b/.github/workflows/code_analysis.yml @@ -17,7 +17,7 @@ jobs: spotbugs_active: ${{ steps.check_spotbugs.outputs.spotbugs_active }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check file existence id: check_files uses: andstor/file-existence-action@v2 @@ -34,7 +34,7 @@ jobs: if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.checkstyle_active == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v3 with: @@ -48,7 +48,7 @@ jobs: if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.spotbugs_active == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v3 with: diff --git a/.github/workflows/license_scan.yml b/.github/workflows/license_scan.yml index fb9a2ded7..711695b6e 100644 --- a/.github/workflows/license_scan.yml +++ b/.github/workflows/license_scan.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run license scanner uses: aquasecurity/trivy-action@master diff --git a/.github/workflows/release_docs_zip.yml b/.github/workflows/release_docs_zip.yml index 844f382b9..c248252e0 100644 --- a/.github/workflows/release_docs_zip.yml +++ b/.github/workflows/release_docs_zip.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Archive deployment-relevant documentation. run: | ARCHIVE_FILE_NAME="broker-server-release-${GITHUB_REF#refs/tags/v}-deployment-docs.zip" diff --git a/.github/workflows/secret_scan.yml b/.github/workflows/secret_scan.yml index b27e1f6b1..613fc5682 100644 --- a/.github/workflows/secret_scan.yml +++ b/.github/workflows/secret_scan.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run vulnerability scanner uses: aquasecurity/trivy-action@master with: diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml index c43bd288b..2555ed0ce 100644 --- a/.github/workflows/security_scan.yml +++ b/.github/workflows/security_scan.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run static analysis uses: aquasecurity/trivy-action@master From dd3a74b7f895ef1d3634e4e9d266459a924cf14c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:46:15 +0200 Subject: [PATCH 123/295] chore(deps): bump docker/metadata-action from 4 to 5 (#255) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5. - [Release notes](https://github.com/docker/metadata-action/releases) - [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md) - [Commits](https://github.com/docker/metadata-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-and-publish-connector-images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish-connector-images.yml b/.github/workflows/build-and-publish-connector-images.yml index cfb17c1d9..c443c97cb 100644 --- a/.github/workflows/build-and-publish-connector-images.yml +++ b/.github/workflows/build-and-publish-connector-images.yml @@ -63,7 +63,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BASE }}/${{ matrix.imageVariants.imageName }} labels: | From e3b63fc5d52e7f22b7e6c0ca14424c4668fbfa06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:02:52 +0200 Subject: [PATCH 124/295] chore(deps): bump io.swagger.core.v3.swagger-gradle-plugin (#253) Bumps io.swagger.core.v3.swagger-gradle-plugin from 2.2.15 to 2.2.16. --- updated-dependencies: - dependency-name: io.swagger.core.v3.swagger-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index f11954300..bef6bcb11 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -4,7 +4,7 @@ val sovityEdcExtensionsVersion: String by project plugins { `java-library` `maven-publish` - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.15" //./gradlew clean resolve + id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.16" //./gradlew clean resolve id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI id("org.openapi.generator") version "6.6.0" //./gradlew openApiValidate && ./gradlew openApiGenerate } From 65656c40e862279a6c9ca08032ef017fee4ec5eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:08:10 +0200 Subject: [PATCH 125/295] chore(deps): bump io.swagger.core.v3:swagger-jaxrs2-jakarta (#252) Bumps io.swagger.core.v3:swagger-jaxrs2-jakarta from 2.2.15 to 2.2.16. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-jaxrs2-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index bef6bcb11..2e1206f05 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -18,14 +18,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.16") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") - implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") + implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.16") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") From d93227321ed12d6cc81b778198c5f6e3fc6f5ba5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:13:01 +0200 Subject: [PATCH 126/295] chore(deps): bump org.openapi.generator from 6.6.0 to 7.0.1 (#251) Bumps org.openapi.generator from 6.6.0 to 7.0.1. --- updated-dependencies: - dependency-name: org.openapi.generator dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 2 +- extensions/broker-server-api/client/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 2e1206f05..af01e357e 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -6,7 +6,7 @@ plugins { `maven-publish` id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.16" //./gradlew clean resolve id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI - id("org.openapi.generator") version "6.6.0" //./gradlew openApiValidate && ./gradlew openApiGenerate + id("org.openapi.generator") version "7.0.1" //./gradlew openApiValidate && ./gradlew openApiGenerate } dependencies { diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index ed2f50f7d..1a9e2936c 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -7,7 +7,7 @@ val assertj: String by project plugins { `java-library` `maven-publish` - id("org.openapi.generator") version "6.6.0" + id("org.openapi.generator") version "7.0.1" } repositories { From db00c2512afa0023ceebda43f72af7cc6b25b944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:18:28 +0200 Subject: [PATCH 127/295] chore(deps): bump io.swagger.core.v3:swagger-annotations-jakarta (#250) Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.15 to 2.2.16. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index af01e357e..d62e14d10 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -17,14 +17,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.16") api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.16") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") + implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.16") implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.16") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") From ed7ee65b6029cee92f1911cf3eea0fb5eda99dae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:27:15 +0200 Subject: [PATCH 128/295] chore(deps): bump org.testcontainers:postgresql from 1.18.3 to 1.19.0 (#236) Bumps [org.testcontainers:postgresql](https://github.com/testcontainers/testcontainers-java) from 1.18.3 to 1.19.0. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.18.3...1.19.0) --- updated-dependencies: - dependency-name: org.testcontainers:postgresql dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index c3f2a9cbc..2e50f7b45 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -22,7 +22,7 @@ val postgresVersion: String by project buildscript { dependencies { - classpath("org.testcontainers:postgresql:1.18.3") + classpath("org.testcontainers:postgresql:1.19.0") } } From a71014b85a22a4526dbed038161e7f651cab9736 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:32:25 +0200 Subject: [PATCH 129/295] chore(deps): bump semver, @microsoft/api-extractor and @rushstack/node-core-library (#260) Bumps [semver](https://github.com/npm/node-semver), [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) and [@rushstack/node-core-library](https://github.com/microsoft/rushstack/tree/HEAD/libraries/node-core-library). These dependencies needed to be updated together. Updates `semver` from 7.3.8 to 7.5.4 - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v7.3.8...v7.5.4) Updates `@microsoft/api-extractor` from 7.34.4 to 7.37.1 - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.37.1/apps/api-extractor) Updates `@rushstack/node-core-library` from 3.55.2 to 3.60.1 - [Changelog](https://github.com/microsoft/rushstack/blob/main/libraries/node-core-library/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@rushstack/node-core-library_v3.60.1/libraries/node-core-library) --- updated-dependencies: - dependency-name: semver dependency-type: indirect - dependency-name: "@microsoft/api-extractor" dependency-type: indirect - dependency-name: "@rushstack/node-core-library" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../client-ts/package-lock.json | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json index fa8287984..1514b14cc 100644 --- a/extensions/broker-server-api/client-ts/package-lock.json +++ b/extensions/broker-server-api/client-ts/package-lock.json @@ -269,50 +269,50 @@ "dev": true }, "node_modules/@microsoft/api-extractor": { - "version": "7.34.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.34.4.tgz", - "integrity": "sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.37.1.tgz", + "integrity": "sha512-wbTL7TZG+9SPvYKwk26390ltoP/uR5621dniqhVp+5OHcn7wIKsT7vX9d/wvdAXD3Ft+7pAiCt6y3dBLFfY/0w==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/api-extractor-model": "7.28.1", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.55.2", - "@rushstack/rig-package": "0.3.18", - "@rushstack/ts-command-line": "4.13.2", + "@rushstack/node-core-library": "3.60.1", + "@rushstack/rig-package": "0.5.1", + "@rushstack/ts-command-line": "4.16.1", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "~4.8.4" + "typescript": "~5.0.4" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz", - "integrity": "sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.1.tgz", + "integrity": "sha512-1hD9gQRu8VR53/e8GI+aql7MtWXHE/XtpOSgphJ6SB7AswqJT0mRZVufUbg3D57UdrchvLKz9b+zqay0Oq2vgg==", "dev": true, "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.55.2" + "@rushstack/node-core-library": "3.60.1" } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } }, "node_modules/@microsoft/tsdoc": { @@ -404,9 +404,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "3.55.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", - "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "version": "3.60.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.60.1.tgz", + "integrity": "sha512-cWKCImfezPvILKu5eUPkz0Mp/cO/zOSJdPD64KHliBcdmbPHg/sF4rEL7WJkWywXT1RQ/U/N8uKdXMe7jDCXNw==", "dev": true, "dependencies": { "colors": "~1.2.1", @@ -414,7 +414,7 @@ "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "z-schema": "~5.0.2" }, "peerDependencies": { @@ -459,9 +459,9 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", - "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", + "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", "dev": true, "dependencies": { "resolve": "~1.22.1", @@ -469,9 +469,9 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", - "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.16.1.tgz", + "integrity": "sha512-+OCsD553GYVLEmz12yiFjMOzuPeCiZ3f8wTiFHL30ZVXexTyPmgjwXEhg2K2P0a2lVf+8YBy7WtPoflB2Fp8/A==", "dev": true, "dependencies": { "@types/argparse": "1.0.38", @@ -1233,9 +1233,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1272,9 +1272,9 @@ "dev": true }, "node_modules/string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "engines": { "node": ">=0.6.19" @@ -1684,42 +1684,42 @@ "dev": true }, "@microsoft/api-extractor": { - "version": "7.34.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.34.4.tgz", - "integrity": "sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.37.1.tgz", + "integrity": "sha512-wbTL7TZG+9SPvYKwk26390ltoP/uR5621dniqhVp+5OHcn7wIKsT7vX9d/wvdAXD3Ft+7pAiCt6y3dBLFfY/0w==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/api-extractor-model": "7.28.1", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.55.2", - "@rushstack/rig-package": "0.3.18", - "@rushstack/ts-command-line": "4.13.2", + "@rushstack/node-core-library": "3.60.1", + "@rushstack/rig-package": "0.5.1", + "@rushstack/ts-command-line": "4.16.1", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "~4.8.4" + "typescript": "~5.0.4" }, "dependencies": { "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true } } }, "@microsoft/api-extractor-model": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz", - "integrity": "sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.1.tgz", + "integrity": "sha512-1hD9gQRu8VR53/e8GI+aql7MtWXHE/XtpOSgphJ6SB7AswqJT0mRZVufUbg3D57UdrchvLKz9b+zqay0Oq2vgg==", "dev": true, "requires": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.55.2" + "@rushstack/node-core-library": "3.60.1" } }, "@microsoft/tsdoc": { @@ -1790,9 +1790,9 @@ } }, "@rushstack/node-core-library": { - "version": "3.55.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", - "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "version": "3.60.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.60.1.tgz", + "integrity": "sha512-cWKCImfezPvILKu5eUPkz0Mp/cO/zOSJdPD64KHliBcdmbPHg/sF4rEL7WJkWywXT1RQ/U/N8uKdXMe7jDCXNw==", "dev": true, "requires": { "colors": "~1.2.1", @@ -1800,7 +1800,7 @@ "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "z-schema": "~5.0.2" }, "dependencies": { @@ -1833,9 +1833,9 @@ } }, "@rushstack/rig-package": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", - "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", + "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", "dev": true, "requires": { "resolve": "~1.22.1", @@ -1843,9 +1843,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", - "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.16.1.tgz", + "integrity": "sha512-+OCsD553GYVLEmz12yiFjMOzuPeCiZ3f8wTiFHL30ZVXexTyPmgjwXEhg2K2P0a2lVf+8YBy7WtPoflB2Fp8/A==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -2401,9 +2401,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -2428,9 +2428,9 @@ "dev": true }, "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true }, "strip-json-comments": { From 5456707dc1dc9940acf1c4d1866300333ed7b6f2 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 29 Sep 2023 14:00:35 +0200 Subject: [PATCH 130/295] fix: exceptions caused by non-string asset properties (#262) --- CHANGELOG.md | 2 ++ .../V4_3__MvP_Fix_Asset_JSON_Properties.sql | 14 +++++++++++++ .../refreshing/offers/DataOfferBuilder.java | 21 ++++++++++++++++++- .../refreshing/ConnectorUpdaterTest.java | 3 +++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 18882cf8d..657cb5a83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Patch +- Fix a bug with non-string asset properties causing exceptions. + ### Deployment Migration Notes ## [v1.0.3] - 2023-09-01 diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql new file mode 100644 index 000000000..f65ad54b9 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql @@ -0,0 +1,14 @@ +-- Maps JSON Values to String +-- '{"a": "b", "c": [1, 2], "d": true}'::jsonb becomes '{"a": "b", "c": "[1, 2]", "d": "true"}'::jsonb +create or replace function pg_temp.migrate_asset_properties(asset_properties jsonb) returns jsonb as +$$ +begin +return (select jsonb_object_agg(key, case when jsonb_typeof(value) = 'string' then value #>> '{}' else value::text end) + from jsonb_each(asset_properties)); +end; +$$ +language plpgsql; + +-- Fix existing data offer asssets +update data_offer +set asset_properties = pg_temp.migrate_asset_properties(asset_properties); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java index a7efa55a0..0ef8145a1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java @@ -26,6 +26,8 @@ import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static java.util.stream.Collectors.groupingBy; @@ -89,7 +91,24 @@ private String getAssetName(Asset asset) { @NotNull @SneakyThrows private String getAssetPropertiesJson(Asset asset) { - return objectMapper.writeValueAsString(asset.getProperties()); + var properties = asset.getProperties().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> getAssetPropertyValue(entry.getValue()) + )); + return objectMapper.writeValueAsString(properties); + } + + @NotNull + @SneakyThrows + private String getAssetPropertyValue(Object value) { + if (value instanceof String stringValue) { + return stringValue; + } + + // Using JSON Properties in the MS8 EDC causes the broker to fail + // this is why we map them to their JSON to "show them", but not fail due to them + return objectMapper.writeValueAsString(value); } @NotNull diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index b3eda5549..f0d6da2be 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; +import java.util.LinkedHashMap; import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; @@ -85,6 +86,7 @@ void testConnectorUpdate( var dataOffer = dataOffers.get(0); assertThat(dataOffer.getAssetId()).isEqualTo("test-asset-1"); assertThat(dataOffer.getAssetProperties().data()).contains("Test Asset 1"); + assertThat(dataOffer.getAssetProperties().data()).contains("\"some-example-prop\": \"{\\\"key\\\":\\\"value\\\"}\""); var contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().toList(); assertThat(contractOffers).hasSize(1); @@ -119,6 +121,7 @@ private void createAsset( .id(assetId) .property(AssetProperty.ASSET_ID, assetId) .property(AssetProperty.ASSET_NAME, assetName) + .property("some-example-prop", new LinkedHashMap<>(Map.of("key", "value"))) .build(); var dataAddress = DataAddress.Builder.newInstance() .properties(Map.of( From 2e4126c551c76aaee5daf765021e161a49117bfa Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:22:26 +0200 Subject: [PATCH 131/295] feat: deletion of connectors (#209) Co-authored-by: Richard Treier --- CHANGELOG.md | 15 ++ .../api/BrokerServerResource.java | 7 + .../build.gradle.kts | 2 + .../ext/brokerserver/db/FlywayFactory.java | 1 + ...> V5_1__MvP_Fix_Asset_JSON_Properties.sql} | 8 +- .../BrokerServerExtensionContextBuilder.java | 3 +- .../BrokerServerResourceImpl.java | 5 + .../services/api/ConnectorApiService.java | 7 + .../services/api/ConnectorService.java | 23 ++- .../services/logging/BrokerEventLogger.java | 14 ++ .../edc/ext/brokerserver/TestUtils.java | 14 ++ .../services/api/AddConnectorsApiTest.java | 30 +--- .../services/api/DeleteConnectorsApiTest.java | 159 ++++++++++++++++++ .../logging/BrokerEventLoggerTest.java | 13 ++ 14 files changed, 274 insertions(+), 27 deletions(-) rename extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/{V4_3__MvP_Fix_Asset_JSON_Properties.sql => V5_1__MvP_Fix_Asset_JSON_Properties.sql} (65%) create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 657cb5a83..7157c3794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor +- New Admin API Endpoint: Delete Connectors + #### Patch - Fix a bug with non-string asset properties causing exceptions. ### Deployment Migration Notes +### Deployment Migration Notes + +1. Connectors can now be dynamically deleted at runtime by using the following endpoint: + ```shell script + # Response should be 204 No Content + curl --request DELETE \ + --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: ApiKeyDefaultValue' \ + --data '["https://some-connector-to-delete/api/v1/ids/data", "https://some-other-connector-to-delete/api/v1/ids/data"]' + ``` + ## [v1.0.3] - 2023-09-01 ### Overview @@ -83,6 +97,7 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde - Broker Server API is now part of this repository. - Dead Connectors are now deleted periodically. - Connector Online Status is now visualized. +- New Admin API Endpoint: Add Connectors #### Patch diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index 2f9275e43..589bf9839 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; @@ -71,4 +72,10 @@ public interface BrokerServerResource { @Consumes(MediaType.APPLICATION_JSON) @Operation(description = "Add unknown Connectors to the Broker Server") void addConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); + + @DELETE + @Path("connectors") + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Delete known Connectors from the Broker Server") + void deleteConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); } diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 2e50f7b45..bb2649d1c 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -119,6 +119,8 @@ flyway { baselineOnMigrate = true locations = arrayOf("filesystem:${migrationsDir}", "filesystem:${testDataDir}") configurations = arrayOf("flywayMigration") + + mixed = true } tasks.withType { diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java index ca949394d..bee8c7b36 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java @@ -42,6 +42,7 @@ public Flyway setupFlyway(DataSource dataSource) { .cleanDisabled(!config.getBoolean(FLYWAY_CLEAN_ENABLE, false)) .table("flyway_schema_history") .locations("classpath:db/migration") + .mixed(true) .load(); } } diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql similarity index 65% rename from extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql rename to extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql index f65ad54b9..fea8c8173 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_3__MvP_Fix_Asset_JSON_Properties.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql @@ -1,6 +1,7 @@ --- Maps JSON Values to String +-- Maps JSON Asset Properties to String -- '{"a": "b", "c": [1, 2], "d": true}'::jsonb becomes '{"a": "b", "c": "[1, 2]", "d": "true"}'::jsonb -create or replace function pg_temp.migrate_asset_properties(asset_properties jsonb) returns jsonb as +create +or replace function pg_temp.migrate_asset_properties(asset_properties jsonb) returns jsonb as $$ begin return (select jsonb_object_agg(key, case when jsonb_typeof(value) = 'string' then value #>> '{}' else value::text end) @@ -12,3 +13,6 @@ language plpgsql; -- Fix existing data offer asssets update data_offer set asset_properties = pg_temp.migrate_asset_properties(asset_properties); + +-- Add new Event Log Status +alter type broker_event_type add value 'CONNECTOR_DELETED'; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index b69648f4a..5fed25c8c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -215,7 +215,8 @@ public static BrokerServerExtensionContext buildContext( var connectorApiService = new ConnectorApiService( connectorPageQueryService, connectorService, - paginationMetadataUtils + paginationMetadataUtils, + brokerEventLogger ); var dataOfferDetailApiService = new DataOfferDetailApiService( dataOfferDetailPageQueryService, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 2f32ecc37..7639f411e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -70,4 +70,9 @@ public void addConnectors(List endpoints, String adminApiKey) { dslContextFactory.transaction(dsl -> connectorApiService.addConnectors(dsl, endpoints)); } + @Override + public void deleteConnectors(List endpoints, String adminApiKey) { + adminApiKeyValidator.validateAdminApiKey(adminApiKey); + dslContextFactory.transaction(dsl -> connectorApiService.deleteConnectors(dsl, endpoints)); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 85fc69ca9..dfaa22851 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -25,6 +25,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; +import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -41,6 +42,7 @@ public class ConnectorApiService { private final ConnectorPageQueryService connectorPageQueryService; private final ConnectorService connectorService; private final PaginationMetadataUtils paginationMetadataUtils; + private final BrokerEventLogger brokerEventLogger; public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery query) { Objects.requireNonNull(query, "query must not be null"); @@ -135,4 +137,9 @@ public void addConnectors(DSLContext dsl, List connectorEndpoints) { .collect(toSet()); connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); } + + public void deleteConnectors(DSLContext dsl, List connectorEndpoints) { + connectorService.deleteConnectors(dsl, connectorEndpoints); + brokerEventLogger.logConnectorsDeleted(dsl, connectorEndpoints); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java index 0b357557c..f22460146 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java @@ -14,16 +14,17 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; +import org.jooq.Record; +import org.jooq.TableField; import java.util.Collection; import java.util.Set; -import static de.sovity.edc.ext.brokerserver.db.jooq.Tables.CONNECTOR; - @RequiredArgsConstructor public class ConnectorService { private final ConnectorCreator connectorCreator; @@ -34,7 +35,23 @@ public void addConnectors(DSLContext dsl, Collection connectorEndpoints, connectorQueue.addAll(connectorEndpoints, priority); } + public void deleteConnectors(DSLContext dsl, Collection endpoints) { + removeConnectorRows(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, endpoints); + removeConnectorRows(dsl, Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, endpoints); + removeConnectorRows(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, endpoints); + removeConnectorRows(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, endpoints); + removeConnectorRows(dsl, Tables.CONNECTOR.ENDPOINT, endpoints); + } + public Set getConnectorEndpoints(DSLContext dsl) { - return dsl.select(CONNECTOR.ENDPOINT).from(CONNECTOR).fetchSet(CONNECTOR.ENDPOINT); + return dsl.select(Tables.CONNECTOR.ENDPOINT).from(Tables.CONNECTOR).fetchSet(Tables.CONNECTOR.ENDPOINT); + } + + private void removeConnectorRows( + DSLContext dsl, + TableField endpointField, + Collection endpoints + ) { + dsl.deleteFrom(endpointField.getTable()).where(endpointField.in(endpoints)).execute(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java index ed88c2a8f..439e81be0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java @@ -21,6 +21,7 @@ import org.jooq.DSLContext; import java.time.OffsetDateTime; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -30,6 +31,19 @@ @RequiredArgsConstructor public class BrokerEventLogger { + public void logConnectorsDeleted(DSLContext dsl, Collection connectorEndpoints) { + var records = connectorEndpoints.stream().map(connectorEndpoint -> { + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_DELETED); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.setConnectorEndpoint(connectorEndpoint); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setUserMessage("Connector was deleted."); + return logEntry; + }).toList(); + dsl.batchInsert(records).execute(); + } + public void logConnectorUpdated(DSLContext dsl, String connectorEndpoint, ConnectorChangeTracker changes) { var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 655a084cb..c9e14605c 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -15,8 +15,10 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; +import de.sovity.edc.ext.brokerserver.client.gen.ApiException; import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import org.assertj.core.api.ThrowableAssert; import org.eclipse.edc.protocol.ids.api.configuration.IdsApiConfigurationExtension; import org.jetbrains.annotations.NotNull; @@ -24,6 +26,8 @@ import java.util.List; import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; public class TestUtils { @@ -94,4 +98,14 @@ public static BrokerServerClient brokerServerClient() { .managementApiKey(TestUtils.MANAGEMENT_API_KEY) .build(); } + + + public static void assertIs401(ThrowableAssert.ThrowingCallable callable) { + assertThatThrownBy(callable) + .isInstanceOf(ApiException.class) + .satisfies(ex -> { + var apiException = (ApiException) ex; + assertThat(apiException.getCode()).isEqualTo(401); + }); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java index 5195ff3bb..fef5ef8f4 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java @@ -15,7 +15,8 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import de.sovity.edc.ext.brokerserver.client.gen.ApiException; +import de.sovity.edc.ext.brokerserver.TestUtils; +import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.db.TestDatabase; @@ -40,24 +41,20 @@ @ApiTest @ExtendWith(EdcExtension.class) class AddConnectorsApiTest { + BrokerServerClient client; @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); @BeforeEach void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", - BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" - ))); + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + client = brokerServerClient(); } @Test - void testAddAndMerge() { + void testAddConnectors() { TEST_DATABASE.testTransaction(dsl -> { - var client = brokerServerClient(); - client.brokerServerApi().addConnectors(ADMIN_API_KEY, List.of()); client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( @@ -82,21 +79,12 @@ void testAddAndMerge() { assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) .extracting(ConnectorListEntry::getEndpoint) .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); - }); } @Test - void testWrongApiKey() { - TEST_DATABASE.testTransaction(dsl -> { - var client = brokerServerClient(); - - assertThatThrownBy(() -> client.brokerServerApi().addConnectors("wrong-api-key", List.of())) - .isInstanceOf(ApiException.class) - .satisfies(ex -> { - var apiException = (ApiException) ex; - assertThat(apiException.getCode()).isEqualTo(401); - }); - }); + void testAddWrongApiKey() { + TEST_DATABASE.testTransaction(dsl -> TestUtils.assertIs401(() -> + client.brokerServerApi().addConnectors("wrong-api-key", List.of()))); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java new file mode 100644 index 000000000..21c57b658 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.TestUtils; +import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.jooq.DSLContext; +import org.jooq.JSONB; +import org.jooq.Record; +import org.jooq.Record1; +import org.jooq.TableField; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class DeleteConnectorsApiTest { + BrokerServerClient client; + String firstConnector = "http://a"; + String otherConnector = "http://b"; + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + client = brokerServerClient(); + } + + @Test + void testRemoveConnectors() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + setupConnectorData(dsl, firstConnector); + setupConnectorData(dsl, otherConnector); + + var connectorsBefore = List.of(firstConnector, otherConnector); + assertContainsEndpoints(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, connectorsBefore); + assertContainsEndpoints(dsl, Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); + assertContainsEndpoints(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); + assertContainsEndpoints(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, connectorsBefore); + assertContainsEndpoints(dsl, Tables.CONNECTOR.ENDPOINT, connectorsBefore); + + // act + client.brokerServerApi().deleteConnectors(ADMIN_API_KEY, List.of(firstConnector)); + + // assert + assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) + .extracting(ConnectorListEntry::getEndpoint) + .containsExactly(otherConnector); + + var connectorsAfter = List.of(otherConnector); + assertContainsEndpoints(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, connectorsAfter); + assertContainsEndpoints(dsl, Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); + assertContainsEndpoints(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); + assertContainsEndpoints(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, connectorsAfter); + assertContainsEndpoints(dsl, Tables.CONNECTOR.ENDPOINT, connectorsAfter); + }); + } + + private void assertContainsEndpoints( + DSLContext dsl, + TableField endpointField, + Collection expected + ) { + var actual = dsl.select(endpointField).from(endpointField.getTable()).fetchSet(endpointField); + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } + + public void setupConnectorData(DSLContext dsl, String endpoint) { + client.brokerServerApi().addConnectors(ADMIN_API_KEY, List.of(endpoint)); + + var assetId = "my-asset"; + + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + dataOffer.setAssetId(assetId); + dataOffer.setAssetName("My Asset"); + dataOffer.setAssetProperties(JSONB.valueOf("{}")); + dataOffer.setConnectorEndpoint(endpoint); + dataOffer.setCreatedAt(OffsetDateTime.now()); + dataOffer.setUpdatedAt(OffsetDateTime.now()); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + contractOffer.setAssetId(assetId); + contractOffer.setConnectorEndpoint(endpoint); + contractOffer.setContractOfferId("my-asset-co"); + contractOffer.setCreatedAt(OffsetDateTime.now()); + contractOffer.setPolicy(JSONB.valueOf("{}")); + contractOffer.setUpdatedAt(OffsetDateTime.now()); + contractOffer.insert(); + + var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); + logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); + logEntry.setUserMessage("Hello World!"); + logEntry.setAssetId(assetId); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.setConnectorEndpoint(endpoint); + logEntry.setEventStatus(BrokerEventStatus.OK); + logEntry.insert(); + + var measurement = dsl.newRecord(Tables.BROKER_EXECUTION_TIME_MEASUREMENT); + measurement.setConnectorEndpoint(endpoint); + measurement.setCreatedAt(OffsetDateTime.now()); + measurement.setDurationInMs(500L); + measurement.setErrorStatus(MeasurementErrorStatus.OK); + measurement.setType(MeasurementType.CONNECTOR_REFRESH); + measurement.insert(); + + var view = dsl.newRecord(Tables.DATA_OFFER_VIEW_COUNT); + view.setConnectorEndpoint(endpoint); + view.setAssetId(assetId); + view.setDate(OffsetDateTime.now()); + view.insert(); + } + + + @Test + void testDeleteWrongApiKey() { + TEST_DATABASE.testTransaction(dsl -> TestUtils.assertIs401(() -> + client.brokerServerApi().deleteConnectors("wrong-api-key", List.of()))); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java index 793ae7d2f..d93c33671 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java @@ -17,10 +17,16 @@ import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import org.jooq.DSLContext; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + class BrokerEventLoggerTest { @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); @@ -44,6 +50,13 @@ void testDataOfferWriter_allSortsOfUpdates() { brokerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, endpoint); brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, 10, endpoint); brokerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, endpoint); + brokerEventLogger.logConnectorsDeleted(dsl, Set.of(endpoint)); + + assertThat(numLogEntries(dsl)).isEqualTo(8); }); } + + private Integer numLogEntries(DSLContext dsl) { + return dsl.selectCount().from(Tables.BROKER_EVENT_LOG).fetchOne().component1(); + } } From 20c626eaef0840dcc56392f87f405cc2a04bbc26 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 29 Sep 2023 16:34:25 +0200 Subject: [PATCH 132/295] chore: prepare release (#264) --- .env | 2 +- CHANGELOG.md | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 9d153f1f7..76ee816af 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.0.3 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.1.0 EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.2.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7157c3794..cf36c85ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,13 +15,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor +#### Patch + +### Deployment Migration Notes + +## [v1.1.0] - 2023-09-29 + +### Overview + +Bugfix release for the asset proprties issue. Also contains the connector delete endpoint. + +### Detailed Changes + +#### Minor + - New Admin API Endpoint: Delete Connectors #### Patch -- Fix a bug with non-string asset properties causing exceptions. - -### Deployment Migration Notes +- Fixed a bug causing exceptions when non-string asset properties were used. ### Deployment Migration Notes @@ -35,6 +47,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --data '["https://some-connector-to-delete/api/v1/ids/data", "https://some-other-connector-to-delete/api/v1/ids/data"]' ``` +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.1.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` +- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) + ## [v1.0.3] - 2023-09-01 ### Overview From 0b85338ac9784b8d03b4d651d306e7b210ad2055 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 29 Sep 2023 16:37:07 +0200 Subject: [PATCH 133/295] chore: update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 86cf12ebe..db26877bb 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,19 @@ curl --request PUT \ --data '["https://some-new-connector/api/v1/ids/data", "https://some-other-new-connector/api/v1/ids/data"]' ``` +#### Removing Connectors at runtime + +Connectors can be dynamically removed at runtime by using the following endpoint: + +```shell script +# Response should be 204 No Content +curl --request DELETE \ + --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: ApiKeyDefaultValue' \ + --data '["https://some-connector-to-be-removed/api/v1/ids/data", "https://some-other-connector-to-be-removed/api/v1/ids/data"]' +``` +

(back to top)

## License From f00d1735f2b2ee4134947a5f56de7c899ef04bdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 08:01:12 +0200 Subject: [PATCH 134/295] chore(deps): bump org.testcontainers:postgresql from 1.19.0 to 1.19.1 (#265) Bumps [org.testcontainers:postgresql](https://github.com/testcontainers/testcontainers-java) from 1.19.0 to 1.19.1. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.0...1.19.1) --- updated-dependencies: - dependency-name: org.testcontainers:postgresql dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index bb2649d1c..ccb6a542b 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -22,7 +22,7 @@ val postgresVersion: String by project buildscript { dependencies { - classpath("org.testcontainers:postgresql:1.19.0") + classpath("org.testcontainers:postgresql:1.19.1") } } From d4778190882efc2a1b850c53da3a96d72bac4f2c Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 12 Oct 2023 09:56:14 +0200 Subject: [PATCH 135/295] fix: URLs getting additional quotes for no reason (#268) --- CHANGELOG.md | 22 +++++++++- docker-compose.yaml | 2 + .../refreshing/offers/DataOfferBuilder.java | 28 ++++++++++--- .../refreshing/ConnectorUpdaterTest.java | 40 ++++++++++++++++++- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf36c85ae..e233f7e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deployment Migration Notes +## [v1.1.1] - 2023-10-11 + +### Overview + +Bugfix release for the asset properties issue. + +### Detailed Changes + +#### Patch + +- Fixed a bug causing some string asset properties getting quotes around them. + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.1.1` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` +- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) + ## [v1.1.0] - 2023-09-29 ### Overview -Bugfix release for the asset proprties issue. Also contains the connector delete endpoint. +Bugfix release for the asset properties issue. Also contains the connector delete endpoint. ### Detailed Changes diff --git a/docker-compose.yaml b/docker-compose.yaml index 298c4f68d..6beaeab6d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,6 +43,8 @@ services: POSTGRESQL_USERNAME: edc POSTGRESQL_PASSWORD: edc POSTGRESQL_DATABASE: edc + ports: + - '54321:5432' volumes: - 'broker-postgresql:/bitnami/postgresql' connector-ui: diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java index 0ef8145a1..25c4f5e83 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java @@ -20,13 +20,16 @@ import de.sovity.edc.ext.brokerserver.utils.StreamUtils2; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import org.apache.commons.lang3.tuple.Pair; import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.jetbrains.annotations.NotNull; +import java.net.URI; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import static java.util.stream.Collectors.groupingBy; @@ -91,17 +94,23 @@ private String getAssetName(Asset asset) { @NotNull @SneakyThrows private String getAssetPropertiesJson(Asset asset) { - var properties = asset.getProperties().entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> getAssetPropertyValue(entry.getValue()) - )); + var properties = mapNonNullValues(asset.getProperties(), this::getAssetPropertyValue); return objectMapper.writeValueAsString(properties); } - @NotNull @SneakyThrows private String getAssetPropertyValue(Object value) { + if (value == null) { + return null; + } + + if (value instanceof URI uri) { + // I don't know why the Eclipse EDC is casting Strings to URIs, but it does + // We need to prevent this from hitting the writeValueAsString or additional + // quotes are added + return uri.toString(); + } + if (value instanceof String stringValue) { return stringValue; } @@ -116,4 +125,11 @@ private String getAssetPropertyValue(Object value) { private String getPolicyJson(ContractOffer offer) { return objectMapper.writeValueAsString(offer.getPolicy()); } + + private Map mapNonNullValues(Map map, Function valueMapper) { + return map.entrySet().stream() + .map(entry -> Pair.of(entry.getKey(), valueMapper.apply(entry.getValue()))) + .filter(entry -> entry.getValue() != null) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index f0d6da2be..4dbc466a6 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -21,6 +21,7 @@ import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import io.restassured.path.json.JsonPath; import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; @@ -37,7 +38,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; @@ -86,7 +89,22 @@ void testConnectorUpdate( var dataOffer = dataOffers.get(0); assertThat(dataOffer.getAssetId()).isEqualTo("test-asset-1"); assertThat(dataOffer.getAssetProperties().data()).contains("Test Asset 1"); - assertThat(dataOffer.getAssetProperties().data()).contains("\"some-example-prop\": \"{\\\"key\\\":\\\"value\\\"}\""); + + var props = JsonPath.from(dataOffer.getAssetProperties().data()); + assertThat(props.getString("\"asset:prop:name\"")).isEqualTo("Test Asset 1"); + assertThat(props.getString("test-array")).isEqualTo("[\"a\",\"b\"]"); + assertThat(props.getString("test-int")).isEqualTo("5"); + assertThat(props.getString("test-double")).isEqualTo("5.1"); + assertThat(props.getString("test-boolean")).isEqualTo("true"); + assertThat(props.getString("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); + + var testObj = JsonPath.from(props.getString("test-obj")); + assertThat((Map) testObj.get("test-obj")).isEqualTo(Map.of("key", "value")); + assertThat((List) testObj.get("test-array")).isEqualTo(List.of("a", "b")); + assertThat((Integer) testObj.get("test-int")).isEqualTo(5); + assertThat((Float) testObj.get("test-double")).isEqualTo(5.1f); + assertThat((Boolean) testObj.get("test-boolean")).isTrue(); + assertThat((String) testObj.get("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); var contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().toList(); assertThat(contractOffers).hasSize(1); @@ -121,7 +139,25 @@ private void createAsset( .id(assetId) .property(AssetProperty.ASSET_ID, assetId) .property(AssetProperty.ASSET_NAME, assetName) - .property("some-example-prop", new LinkedHashMap<>(Map.of("key", "value"))) + .property("test-uri", "https://w3id.org/idsa/code/AB") + .property("http://test-uri-key", "value") + .property("test-obj", new LinkedHashMap<>(Map.of( + "test-uri", "https://w3id.org/idsa/code/AB", + "http://test-uri-key", "value", + "test-obj", new LinkedHashMap<>(Map.of( + "key", "value" + )), + "test-array", new ArrayList<>(List.of("a", "b")), + "test-string", "hello", + "test-int", 5, + "test-double", 5.1, + "test-boolean", true + ))) + .property("test-array", new ArrayList<>(List.of("a", "b"))) + .property("test-string", "hello") + .property("test-int", 5) + .property("test-double", 5.1) + .property("test-boolean", true) .build(); var dataAddress = DataAddress.Builder.newInstance() .properties(Map.of( From 6d2abc02546cd228a2e53962a1a7d5b5460f0570 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 12 Oct 2023 10:42:49 +0200 Subject: [PATCH 136/295] test: asset null behavior (#269) --- .../refreshing/ConnectorUpdaterTest.java | 115 +++++++++++------- 1 file changed, 71 insertions(+), 44 deletions(-) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index 4dbc466a6..3dcd46eaa 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -31,6 +31,7 @@ import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.asset.AssetSelectorExpression; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.junit.jupiter.api.BeforeEach; @@ -45,6 +46,7 @@ import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @ApiTest @ExtendWith(EdcExtension.class) @@ -66,13 +68,44 @@ void testConnectorUpdate( ) { TEST_DATABASE.testTransaction(dsl -> { // arrange + var connectorEndpoint = TestUtils.PROTOCOL_ENDPOINT; var connectorUpdater = BrokerServerExtensionContext.instance.connectorUpdater(); var connectorCreator = BrokerServerExtensionContext.instance.connectorCreator(); - String connectorEndpoint = TestUtils.PROTOCOL_ENDPOINT; createAlwaysTruePolicyDefinition(policyDefinitionStore); createAlwaysTrueContractDefinition(contractDefinitionStore); - createAsset(assetService, "test-asset-1", "Test Asset 1"); + + var nestedObjProperty = new LinkedHashMap(Map.of( + "test-string", "hello", + "test-uri", "https://w3id.org/idsa/code/AB", + "http://test-uri-key", "value", + + "test-array", new ArrayList<>(List.of("a", "b")), + "test-float", 5.1, + "test-int", 5, + "test-boolean", true, + + "test-obj", new LinkedHashMap<>(Map.of("key", "value")) + )); + nestedObjProperty.put("test-null", null); + + var asset = Asset.Builder.newInstance() + .id("test-asset-1") + .property(AssetProperty.ASSET_ID, "test-asset-1") + .property(AssetProperty.ASSET_NAME, "Test Asset 1") + .property("test-string", "hello") + .property("test-uri", "https://w3id.org/idsa/code/AB") + .property("http://test-uri-key", "value") + + .property("test-array", new ArrayList<>(List.of("a", "b"))) + .property("test-float", 5.1) + .property("test-int", 5) + .property("test-boolean", true) + + .property("test-obj", nestedObjProperty) + .build(); + + assetService.create(asset, dataAddress()); connectorCreator.addConnector(dsl, connectorEndpoint); // act @@ -92,25 +125,56 @@ void testConnectorUpdate( var props = JsonPath.from(dataOffer.getAssetProperties().data()); assertThat(props.getString("\"asset:prop:name\"")).isEqualTo("Test Asset 1"); + assertThat(props.getString("test-string")).isEqualTo("hello"); + assertThat(props.getString("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); + assertThat(props.getString("http://test-uri-key")).isEqualTo("value"); + assertThat(props.getString("test-array")).isEqualTo("[\"a\",\"b\"]"); assertThat(props.getString("test-int")).isEqualTo("5"); - assertThat(props.getString("test-double")).isEqualTo("5.1"); + assertThat(props.getString("test-float")).isEqualTo("5.1"); assertThat(props.getString("test-boolean")).isEqualTo("true"); - assertThat(props.getString("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); var testObj = JsonPath.from(props.getString("test-obj")); - assertThat((Map) testObj.get("test-obj")).isEqualTo(Map.of("key", "value")); + assertThat((String) testObj.get("test-string")).isEqualTo("hello"); + assertThat((String) testObj.get("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); + assertThat((String) testObj.get("http://test-uri-key")).isEqualTo("value"); + assertThat((List) testObj.get("test-array")).isEqualTo(List.of("a", "b")); assertThat((Integer) testObj.get("test-int")).isEqualTo(5); - assertThat((Float) testObj.get("test-double")).isEqualTo(5.1f); + assertThat((Float) testObj.get("test-float")).isEqualTo(5.1f); assertThat((Boolean) testObj.get("test-boolean")).isTrue(); - assertThat((String) testObj.get("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); + assertThat((Map) testObj.get("test-obj")).isEqualTo(Map.of("key", "value")); + + // the nested object's null will have disappeared + assertThat(testObj.getMap("")).containsKey("test-string"); + assertThat(testObj.getMap("")).doesNotContainKey("test-null"); var contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().toList(); assertThat(contractOffers).hasSize(1); }); } + @Test + void testTopLevelAssetPropertyCannotBeNull(AssetService assetService) { + var asset = Asset.Builder.newInstance() + .id("test-asset-1") + .property("test-null", null) + .build(); + var dataAddress = dataAddress(); + assertThatThrownBy(() -> assetService.create(asset, dataAddress)) + .isInstanceOf(EdcPersistenceException.class) + .hasMessage("java.lang.NullPointerException: Cannot invoke \"Object.getClass()\" because the return value of \"java.util.Map$Entry.getValue()\" is null"); + } + + private DataAddress dataAddress() { + return DataAddress.Builder.newInstance() + .properties(Map.of( + "type", "HttpData", + "baseUrl", "https://jsonplaceholder.typicode.com/todos/1" + )) + .build(); + } + private void createAlwaysTruePolicyDefinition(PolicyDefinitionStore policyDefinitionStore) { var policyDefinition = PolicyDefinition.Builder.newInstance() .id("always-true") @@ -130,41 +194,4 @@ public void createAlwaysTrueContractDefinition(ContractDefinitionStore contractD contractDefinitionStore.save(contractDefinition); } - private void createAsset( - AssetService assetService, - String assetId, - String assetName - ) { - var asset = Asset.Builder.newInstance() - .id(assetId) - .property(AssetProperty.ASSET_ID, assetId) - .property(AssetProperty.ASSET_NAME, assetName) - .property("test-uri", "https://w3id.org/idsa/code/AB") - .property("http://test-uri-key", "value") - .property("test-obj", new LinkedHashMap<>(Map.of( - "test-uri", "https://w3id.org/idsa/code/AB", - "http://test-uri-key", "value", - "test-obj", new LinkedHashMap<>(Map.of( - "key", "value" - )), - "test-array", new ArrayList<>(List.of("a", "b")), - "test-string", "hello", - "test-int", 5, - "test-double", 5.1, - "test-boolean", true - ))) - .property("test-array", new ArrayList<>(List.of("a", "b"))) - .property("test-string", "hello") - .property("test-int", 5) - .property("test-double", 5.1) - .property("test-boolean", true) - .build(); - var dataAddress = DataAddress.Builder.newInstance() - .properties(Map.of( - "type", "HttpData", - "baseUrl", "https://jsonplaceholder.typicode.com/todos/1" - )) - .build(); - assetService.create(asset, dataAddress); - } } From 47f30dd44b45e1a8938ff8252d7cc8016cfb0094 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 12 Oct 2023 14:15:06 +0200 Subject: [PATCH 137/295] chore: prepare release (#271) --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 76ee816af..a8d6deb45 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.1.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.1.1 EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.2.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13 From e3251442476fb30652d11f8d599a9afda8666828 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:59:22 +0200 Subject: [PATCH 138/295] chore(deps): bump com.squareup.okhttp3:okhttp from 4.11.0 to 4.12.0 (#280) Bumps [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp) from 4.11.0 to 4.12.0. - [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/okhttp/compare/parent-4.11.0...parent-4.12.0) --- updated-dependencies: - dependency-name: com.squareup.okhttp3:okhttp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/client/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index 1a9e2936c..2d68e85c9 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -26,8 +26,8 @@ dependencies { // Generated Client's Dependencies implementation("io.swagger:swagger-annotations:1.6.11") implementation("com.google.code.findbugs:jsr305:3.0.2") - implementation("com.squareup.okhttp3:okhttp:4.11.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") implementation("com.google.code.gson:gson:2.10.1") implementation("io.gsonfire:gson-fire:1.8.5") implementation("org.openapitools:jackson-databind-nullable:0.2.6") From 1a0075ae3ba3758e08155406841a86eb1efd612b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:08:51 +0200 Subject: [PATCH 139/295] chore(deps): bump io.swagger:swagger-annotations from 1.6.11 to 1.6.12 (#277) Bumps io.swagger:swagger-annotations from 1.6.11 to 1.6.12. --- updated-dependencies: - dependency-name: io.swagger:swagger-annotations dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/client/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index 2d68e85c9..845d3c6e0 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { } // Generated Client's Dependencies - implementation("io.swagger:swagger-annotations:1.6.11") + implementation("io.swagger:swagger-annotations:1.6.12") implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") From 76cb9d7e2aa4b9d72248b846513b2bf32b2cc744 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:16:49 +0200 Subject: [PATCH 140/295] chore(deps): bump io.swagger.core.v3:swagger-jaxrs2-jakarta (#276) Bumps io.swagger.core.v3:swagger-jaxrs2-jakarta from 2.2.16 to 2.2.17. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-jaxrs2-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index d62e14d10..96b7ce926 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -18,14 +18,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.16") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.16") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.17") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.16") - implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.16") + implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.17") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") From 711d0b1a43c2fa82a6ef7fae6e6a5286fca3c8a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:26:32 +0200 Subject: [PATCH 141/295] chore(deps): bump io.swagger.core.v3:swagger-annotations-jakarta (#275) Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.16 to 2.2.17. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 96b7ce926..1d8beae1d 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -17,14 +17,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.16") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.17") api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.17") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.16") + implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.17") implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.17") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") From 23ec8c4b35cb1c04d8a9b10b40bf0c284998acf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:45:50 +0200 Subject: [PATCH 142/295] chore(deps): bump org.jooq:jooq from 3.18.6 to 3.18.7 (#267) Bumps org.jooq:jooq from 3.18.6 to 3.18.7. --- updated-dependencies: - dependency-name: org.jooq:jooq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-postgres-flyway-jooq/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index ccb6a542b..a787bffee 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -34,7 +34,7 @@ plugins { } dependencies { - api("org.jooq:jooq:3.18.6") + api("org.jooq:jooq:3.18.7") api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") jooqGenerator("org.postgresql:postgresql:42.6.0") From fc07c3c629962fbc1b4ad5b274113d98e43f2699 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:55:00 +0200 Subject: [PATCH 143/295] chore(deps-dev): bump postcss in /extensions/broker-server-api/client-ts (#266) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../client-ts/package-lock.json | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json index 1514b14cc..c0dfbbd56 100644 --- a/extensions/broker-server-api/client-ts/package-lock.json +++ b/extensions/broker-server-api/client-ts/package-lock.json @@ -1099,9 +1099,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -1111,10 +1111,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -2337,12 +2341,12 @@ "dev": true }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } From ba7e358eba76a0790e0f611de70cb8b75faf4495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:02:39 +0200 Subject: [PATCH 144/295] chore(deps): bump io.swagger.core.v3.swagger-gradle-plugin (#274) Bumps io.swagger.core.v3.swagger-gradle-plugin from 2.2.16 to 2.2.17. --- updated-dependencies: - dependency-name: io.swagger.core.v3.swagger-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 1d8beae1d..d1764277c 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -4,7 +4,7 @@ val sovityEdcExtensionsVersion: String by project plugins { `java-library` `maven-publish` - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.16" //./gradlew clean resolve + id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.17" //./gradlew clean resolve id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI id("org.openapi.generator") version "7.0.1" //./gradlew openApiValidate && ./gradlew openApiGenerate } From ddd316972439b963ec96b36431513f024c9976ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:10:21 +0200 Subject: [PATCH 145/295] chore(deps-dev): bump the npm_and_yarn at /extensions/broker-server-api/client-ts security update group (#282) Bumps the npm_and_yarn at /extensions/broker-server-api/client-ts security update group in /extensions/broker-server-api/client-ts with 1 update: [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports). - [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases) - [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md) - [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v4.1.1...v4.2.1) --- updated-dependencies: - dependency-name: "@trivago/prettier-plugin-sort-imports" dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../client-ts/package-lock.json | 437 ++++++++++++------ 1 file changed, 284 insertions(+), 153 deletions(-) diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json index c0dfbbd56..cfaa4ad61 100644 --- a/extensions/broker-server-api/client-ts/package-lock.json +++ b/extensions/broker-server-api/client-ts/package-lock.json @@ -18,12 +18,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -53,35 +54,35 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -89,25 +90,25 @@ } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -115,25 +116,25 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -141,31 +142,31 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -173,9 +174,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -185,27 +186,27 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -213,19 +214,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -233,6 +234,35 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", @@ -262,12 +292,54 @@ "node": ">=12" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.37.1", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.37.1.tgz", @@ -481,21 +553,21 @@ } }, "node_modules/@trivago/prettier-plugin-sort-imports": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.1.1.tgz", - "integrity": "sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", + "integrity": "sha512-iuy2MPVURGdxILTchHr15VAioItuYBejKfcTmQFlxIuqA7jeaT6ngr5aUIG6S6U096d6a6lJCgaOwlRrPLlOPg==", "dev": true, "dependencies": { "@babel/generator": "7.17.7", "@babel/parser": "^7.20.5", - "@babel/traverse": "7.17.3", + "@babel/traverse": "7.23.2", "@babel/types": "7.17.0", "javascript-natural-sort": "0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", - "prettier": "2.x" + "prettier": "2.x - 3.x" }, "peerDependenciesMeta": { "@vue/compiler-sfc": { @@ -1493,12 +1565,13 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/generator": { @@ -1521,147 +1594,172 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "dependencies": { "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "dependencies": { "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "dependencies": { "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "dependencies": { "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } } } }, "@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/types": { @@ -1681,12 +1779,45 @@ "dev": true, "optional": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@microsoft/api-extractor": { "version": "7.37.1", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.37.1.tgz", @@ -1859,14 +1990,14 @@ } }, "@trivago/prettier-plugin-sort-imports": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.1.1.tgz", - "integrity": "sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", + "integrity": "sha512-iuy2MPVURGdxILTchHr15VAioItuYBejKfcTmQFlxIuqA7jeaT6ngr5aUIG6S6U096d6a6lJCgaOwlRrPLlOPg==", "dev": true, "requires": { "@babel/generator": "7.17.7", "@babel/parser": "^7.20.5", - "@babel/traverse": "7.17.3", + "@babel/traverse": "7.23.2", "@babel/types": "7.17.0", "javascript-natural-sort": "0.7.1", "lodash": "^4.17.21" From 260e33fe8803de77f4d431d59d9118df1c320c24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:10:30 +0200 Subject: [PATCH 146/295] chore(deps): bump actions/setup-node from 3 to 4 (#283) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-and-publish-ts-api-client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish-ts-api-client.yml b/.github/workflows/build-and-publish-ts-api-client.yml index 37b80cfd3..0b49fab03 100644 --- a/.github/workflows/build-and-publish-ts-api-client.yml +++ b/.github/workflows/build-and-publish-ts-api-client.yml @@ -24,7 +24,7 @@ jobs: distribution: 'temurin' java-version: '17' cache: 'gradle' - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 16 cache: 'npm' From 646d7867469bdef18e1c8b83e1bee88e35ef2eb4 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:14:58 +0200 Subject: [PATCH 147/295] feat: DataOffer count entdpoint (#284) Co-authored-by: Jan Ridderbusch Co-authored-by: Richard Treier --- .../api/BrokerServerResource.java | 8 + .../api/model/DataOfferCountResult.java | 21 +++ .../BrokerServerExtensionContextBuilder.java | 5 +- .../BrokerServerResourceImpl.java | 8 + .../api/DataOfferCountApiService.java | 49 ++++++ .../services/api/AddConnectorsApiTest.java | 4 +- .../services/api/DataOfferCountApiTest.java | 145 ++++++++++++++++++ 7 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index 589bf9839..c24eefe99 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import io.swagger.v3.oas.annotations.Operation; @@ -78,4 +79,11 @@ public interface BrokerServerResource { @Consumes(MediaType.APPLICATION_JSON) @Operation(description = "Delete known Connectors from the Broker Server") void deleteConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); + + @POST + @Path("authority-portal-api/data-offer-counts") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Query the amount of public Data Offers by provided Connector URLs") + DataOfferCountResult dataOfferCount(List endpoints); } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java new file mode 100644 index 000000000..061644ef3 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java @@ -0,0 +1,21 @@ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Number of Data Offers per Connector endpoint.", requiredMode = Schema.RequiredMode.REQUIRED) +public class DataOfferCountResult { + @Schema(description = "Map from endpoint URL to Data Offer count", requiredMode = Schema.RequiredMode.REQUIRED) + private Map dataOfferCount; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 5fed25c8c..330c9f53b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -38,6 +38,7 @@ import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorService; +import de.sovity.edc.ext.brokerserver.services.api.DataOfferCountApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; @@ -224,12 +225,14 @@ public static BrokerServerExtensionContext buildContext( policyDtoBuilder, assetPropertyParser ); + var dataOfferCountApiService = new DataOfferCountApiService(); var brokerServerResource = new BrokerServerResourceImpl( dslContextFactory, connectorApiService, catalogApiService, dataOfferDetailApiService, - adminApiKeyValidator + adminApiKeyValidator, + dataOfferCountApiService ); return new BrokerServerExtensionContext( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 7639f411e..eb34766e1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -21,11 +21,13 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.DataOfferCountApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; import lombok.RequiredArgsConstructor; @@ -43,6 +45,7 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final CatalogApiService catalogApiService; private final DataOfferDetailApiService dataOfferDetailApiService; private final AdminApiKeyValidator adminApiKeyValidator; + private final DataOfferCountApiService dataOfferCountApiService; @Override public CatalogPageResult catalogPage(CatalogPageQuery query) { @@ -75,4 +78,9 @@ public void deleteConnectors(List endpoints, String adminApiKey) { adminApiKeyValidator.validateAdminApiKey(adminApiKey); dslContextFactory.transaction(dsl -> connectorApiService.deleteConnectors(dsl, endpoints)); } + + @Override + public DataOfferCountResult dataOfferCount(List endpoints) { + return dslContextFactory.transactionResult(dsl -> dataOfferCountApiService.countByEndpoints(dsl, endpoints)); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java new file mode 100644 index 000000000..f43147bb1 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; + +import java.util.List; +import java.util.function.Function; + +import static java.util.stream.Collectors.toMap; + +@RequiredArgsConstructor +public class DataOfferCountApiService { + + public DataOfferCountResult countByEndpoints(DSLContext dsl, List endpoints) { + var d = Tables.DATA_OFFER; + + var count = DSL.count().as("count"); + var numDataOffers = dsl.select(d.CONNECTOR_ENDPOINT, count) + .from(d) + .where(PostgresqlUtils.in(d.CONNECTOR_ENDPOINT, endpoints)) + .groupBy(d.CONNECTOR_ENDPOINT) + .fetchMap(d.CONNECTOR_ENDPOINT, count); + + var numDataOffersFilled = endpoints.stream().distinct().collect(toMap( + Function.identity(), + endpoint -> numDataOffers.getOrDefault(endpoint, 0) + )); + + return new DataOfferCountResult(numDataOffersFilled); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java index fef5ef8f4..bf01f089b 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; import de.sovity.edc.ext.brokerserver.TestUtils; import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; @@ -33,10 +32,9 @@ import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; @ApiTest @ExtendWith(EdcExtension.class) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java new file mode 100644 index 000000000..053c14c6d --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.SneakyThrows; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Policy; +import org.jooq.DSLContext; +import org.jooq.JSONB; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static groovy.json.JsonOutput.toJson; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class DataOfferCountApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + } + + @Test + void testCountByEndpoints() { + TEST_DATABASE.testTransaction(dsl -> { + var now = OffsetDateTime.now().withNano(0); + + createConnector(dsl, now, 1); + createDataOffer(dsl, now, 1, 1); + createDataOffer(dsl, now, 1, 2); + + createConnector(dsl, now, 2); + createDataOffer(dsl, now, 2, 1); + + createConnector(dsl, now, 3); + createDataOffer(dsl, now, 3, 1); + + createConnector(dsl, now, 4); + + var actual = brokerServerClient().brokerServerApi().dataOfferCount(Arrays.asList( + getEndpoint(1), + getEndpoint(1), // having this twice should not crash the query + getEndpoint(2), + getEndpoint(4) + )); + var dataOfferCountMap = actual.getDataOfferCount(); + assertThat(dataOfferCountMap).isEqualTo(Map.of( + getEndpoint(1), 2, + getEndpoint(2), 1, + getEndpoint(4), 0 + )); + }); + } + + private void createConnector(DSLContext dsl, OffsetDateTime today, int iConnector) { + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setConnectorId("https://my-connector"); + connector.setEndpoint(getEndpoint(iConnector)); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setCreatedAt(today.minusDays(1)); + connector.setLastRefreshAttemptAt(today); + connector.setLastSuccessfulRefreshAt(today); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + connector.insert(); + } + + private String getEndpoint(int iConnector) { + return "https://connector-%d".formatted(iConnector); + } + + private void createDataOffer(DSLContext dsl, OffsetDateTime today, int iConnector, int iDataOffer) { + var connectorEndpoint = getEndpoint(iConnector); + var assetProperties = Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(iDataOffer) + ); + + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); + dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + dataOffer.setConnectorEndpoint(connectorEndpoint); + dataOffer.setCreatedAt(today.minusDays(5)); + dataOffer.setUpdatedAt(today); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + contractOffer.setContractOfferId("my-contract-offer-1"); + contractOffer.setConnectorEndpoint(connectorEndpoint); + contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setCreatedAt(today.minusDays(5)); + contractOffer.setUpdatedAt(today); + contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.insert(); + } + + private Policy dummyPolicy() { + return Policy.Builder.newInstance() + .assignee("Example Assignee") + .build(); + } + + private String policyToJson(Policy policy) { + return toJson(policy); + } + + @SneakyThrows + private String assetProperties(Map assetProperties) { + return new ObjectMapper().writeValueAsString(assetProperties); + } +} From 832e7bc4fb1346094e72053e58bfe306c6e77fea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 07:52:33 +0200 Subject: [PATCH 148/295] chore(deps): bump io.swagger.core.v3.swagger-gradle-plugin (#288) Bumps io.swagger.core.v3.swagger-gradle-plugin from 2.2.17 to 2.2.18. --- updated-dependencies: - dependency-name: io.swagger.core.v3.swagger-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index d1764277c..a0a0c0138 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -4,7 +4,7 @@ val sovityEdcExtensionsVersion: String by project plugins { `java-library` `maven-publish` - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.17" //./gradlew clean resolve + id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.18" //./gradlew clean resolve id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI id("org.openapi.generator") version "7.0.1" //./gradlew openApiValidate && ./gradlew openApiGenerate } From 7f0d298959a6d7a6f41cd891bb7cd5b762c4e073 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 07:58:23 +0200 Subject: [PATCH 149/295] chore(deps): bump io.swagger.core.v3:swagger-jaxrs2-jakarta (#287) Bumps io.swagger.core.v3:swagger-jaxrs2-jakarta from 2.2.17 to 2.2.18. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-jaxrs2-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index a0a0c0138..af94750f1 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -18,14 +18,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.17") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.17") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.18") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.17") - implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.17") + implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.18") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") From 212f404f5ef83f81d64be195143a6bfbd357c768 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 08:04:22 +0200 Subject: [PATCH 150/295] chore(deps): bump io.swagger.core.v3:swagger-annotations-jakarta (#286) Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.17 to 2.2.18. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/broker-server-api/api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index af94750f1..2806f4d97 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -17,14 +17,14 @@ dependencies { api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.17") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.18") api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.18") api("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("org.apache.commons:commons-lang3:3.13.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.17") + implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.18") implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.18") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation("jakarta.validation:jakarta.validation-api:3.0.2") From a25e9fb62beed3420ef73e0057c8f56594c56695 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:10:20 +0200 Subject: [PATCH 151/295] feat: connector filter for catalog page (#289) Co-authored-by: Jan Ridderbusch Co-authored-by: Richard Treier --- CHANGELOG.md | 3 ++ ...talogFilterAttributeDefinitionService.java | 9 ++++ .../api/filtering/CatalogFilterService.java | 8 ++-- .../services/api/CatalogApiTest.java | 41 +++++++++++++++++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e233f7e6a..4db2c7713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor +- Added an endpoint for getting the data offer amounts for connectors. +- Added a Connector filter to the Catalog Page. + #### Patch ### Deployment Migration Notes diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java index c1ce78afc..a54ec8f8e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java @@ -40,6 +40,15 @@ public CatalogFilterAttributeDefinition buildDataSpaceFilter() { ); } + public CatalogFilterAttributeDefinition buildConnectorEndpointFilter() { + return new CatalogFilterAttributeDefinition( + "connectorEndpoint", + "Connector", + fields -> fields.getDataOfferTable().CONNECTOR_ENDPOINT, + (fields, values) -> PostgresqlUtils.in(fields.getDataOfferTable().CONNECTOR_ENDPOINT, values) + ); + } + @NotNull private Field getValue(CatalogQueryFields fields, String assetProperty) { return DSL.coalesce(fields.getAssetProperty(assetProperty), DSL.value("")); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index f59b312fa..73a6cce17 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -23,6 +23,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; @@ -75,9 +76,10 @@ private List getAvailableFilters() { "Transport Mode" ), catalogFilterAttributeDefinitionService.fromAssetProperty( - AssetProperty.GEO_REFERENCE_METHOD, - "Geo Reference Method" - ) + AssetProperty.GEO_REFERENCE_METHOD, + "Geo Reference Method" + ), + catalogFilterAttributeDefinitionService.buildConnectorEndpointFilter() ); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 006ec3e98..b5c1b9b86 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -47,11 +47,10 @@ import java.time.OffsetDateTime; import java.util.List; import java.util.Map; -import java.util.stream.IntStream; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static java.util.stream.IntStream.*; +import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -100,6 +99,33 @@ void testDataSpace_two_dataspaces_filter_for_one() { }); } + @Test + void testConnectorEndpointFilter_two_connectors_filter_for_one() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + createConnector(dsl, today, "http://my-connector/ids/data"); + createConnector(dsl, today, "http://my-connector2/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" + ), "http://my-connector/ids/data"); + createDataOffer(dsl, today, Map.of( + AssetProperty.ASSET_ID, "urn:artifact:my-asset", + AssetProperty.ASSET_NAME, "my-asset" + ), "http://my-connector2/ids/data"); + + var query = new CatalogPageQuery(); + query.setFilter(new CnfFilterValue(List.of( + new CnfFilterValueAttribute("connectorEndpoint", List.of("http://my-connector/ids/data")) + ))); + + var result = brokerServerClient().brokerServerApi().catalogPage(query); + assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly("urn:artifact:my-asset"); + }); + } + @Test void test_available_filter_values_to_filter_by() { TEST_DATABASE.testTransaction(dsl -> { @@ -236,7 +262,8 @@ void testAvailableFilters_noFilter() { AssetProperty.DATA_SUBCATEGORY, AssetProperty.DATA_MODEL, AssetProperty.TRANSPORT_MODE, - AssetProperty.GEO_REFERENCE_METHOD + AssetProperty.GEO_REFERENCE_METHOD, + "connectorEndpoint" ); assertThat(result.getAvailableFilters().getFields()) @@ -247,7 +274,8 @@ void testAvailableFilters_noFilter() { "Data Subcategory", "Data Model", "Transport Mode", - "Geo Reference Method" + "Geo Reference Method", + "Connector" ); var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); @@ -264,6 +292,11 @@ void testAvailableFilters_noFilter() { assertThat(dataSubcategory.getTitle()).isEqualTo("Data Subcategory"); assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); + + var connectorEndpoint = getAvailableFilter(result, "connectorEndpoint"); + assertThat(connectorEndpoint.getTitle()).isEqualTo("Connector"); + assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getId).containsExactly("http://my-connector/ids/data"); + assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("http://my-connector/ids/data"); }); } From 1d8cf119f8abb3fdf71f22995618bf2ba10389b3 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:30:38 +0100 Subject: [PATCH 152/295] chore: Prepare release (#291) * chore: Prepare release * chore: Fix versions --------- Co-authored-by: Jan Ridderbusch --- .env | 2 +- CHANGELOG.md | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.env b/.env index a8d6deb45..fb30adf4a 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.1.1 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.2.0 EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.2.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db2c7713..719d1b64c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,13 +15,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor +#### Patch + +### Deployment Migration Notes + +## [v1.2.0] - 2023-10-30 + +### Overview + +Adapt to requirements of the Authority Portal - Release v2.0.0. + +### Detailed Changes + +#### Minor + - Added an endpoint for getting the data offer amounts for connectors. - Added a Connector filter to the Catalog Page. -#### Patch - ### Deployment Migration Notes +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.2.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` +- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) + ## [v1.1.1] - 2023-10-11 ### Overview From a578c07476f3e95bc4c9853954d7ea99fd930ce2 Mon Sep 17 00:00:00 2001 From: AbdullahMuk <143605455+AbdullahMuk@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:29:53 +0100 Subject: [PATCH 153/295] Update feature_request.md (#292) Updated some of the sections to make it more suitable for MDS audience to post their feature requests. --- .github/ISSUE_TEMPLATE/feature_request.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ba9d3218f..72393daf5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- name: Feature Request -about: Help us with new features +about: Help us improve your product experience with new features suggestions title: "" labels: ["kind/enhancement", "scope/mds"] assignees: "" @@ -20,17 +20,17 @@ _e.g., DPF, CI, build, transfer, etc._ ## Why Is the Feature Desired? -_Are there any requirements?_ +_What problems does that user face that existing functionalities do solve?_ -## How does this tie into our current product? +## How does this tie into the current product? -_Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new markets and innovative ideas?_ +_Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new innovative ideas?_ -## Stakeholders +## (Leave Blank)Stakeholders _Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ -## Solution Proposal and Work Breakdown +## (Leave Balnk)Solution Proposal and Work Breakdown _If possible, provide a (brief!) solution proposal._ From 4827dedfd77d4358a4924ccf878b3512eab15848 Mon Sep 17 00:00:00 2001 From: AbdullahMuk <143605455+AbdullahMuk@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:02:17 +0100 Subject: [PATCH 154/295] chore: update feature_request.md (#305) * chore: updated the title of "Stakeholders" and "Solution Proposal and Work Breakdown" section * chore: updated the title of "Stakeholders" and "Solution Proposal and Work Breakdown" section --- .github/ISSUE_TEMPLATE/feature_request.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 72393daf5..4afec849d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -26,11 +26,11 @@ _What problems does that user face that existing functionalities do solve?_ _Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new innovative ideas?_ -## (Leave Blank)Stakeholders +## (For sovity Team to complete) Stakeholders _Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ -## (Leave Balnk)Solution Proposal and Work Breakdown +## (For sovity Team to complete) Solution Proposal and Work Breakdown _If possible, provide a (brief!) solution proposal._ From babb796d0af95d38187697f21adbd243dfb3a1a0 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 17 Nov 2023 09:10:07 +0100 Subject: [PATCH 155/295] feat: migrate to Eclipse EDC 0.2.1 (#298) Co-authored-by: Jan Ridderbusch --- CHANGELOG.md | 15 +- README.md | 8 +- build.gradle.kts | 9 +- connector/.env | 2 +- connector/build.gradle.kts | 3 +- docker-compose.yaml | 6 +- .../api/BrokerServerResource.java | 2 +- .../api/model/CatalogContractOffer.java | 4 +- .../api/model/CatalogDataOffer.java | 6 +- .../api/model/ConnectorDetailPageResult.java | 8 +- .../api/model/ConnectorListEntry.java | 8 +- .../model/DataOfferDetailContractOffer.java | 4 +- .../api/model/DataOfferDetailPageResult.java | 6 +- .../build.gradle.kts | 5 - .../db/PostgresFlywayExtension.java | 8 - .../main/resources/db/migration/V6__EDC0.sql | 166 ++++++++++ .../db/testdata/V2_1__PoC_Test_Data.sql | 16 +- extensions/broker-server/build.gradle.kts | 11 +- .../brokerserver/BrokerServerExtension.java | 10 + .../BrokerServerExtensionContext.java | 8 +- .../BrokerServerExtensionContextBuilder.java | 123 +++++-- .../BrokerServerResourceImpl.java | 8 +- .../ext/brokerserver/dao/AssetProperty.java | 44 --- ...Queries.java => ContractOfferQueries.java} | 8 +- .../CatalogQueryAvailableFilterFetcher.java | 2 +- .../CatalogQueryContractOfferFetcher.java | 8 +- .../catalog/CatalogQueryDataOfferFetcher.java | 11 +- .../dao/pages/catalog/CatalogQueryFields.java | 14 - .../catalog/CatalogQueryFilterService.java | 16 +- .../catalog/CatalogQuerySortingService.java | 4 +- .../catalog/models/DataOfferListEntryRs.java | 3 +- .../ConnectorDetailQueryService.java | 60 ++++ ...ce.java => ConnectorListQueryService.java} | 46 +-- .../connector/model/ConnectorDetailsRs.java | 2 +- .../connector/model/ConnectorListEntryRs.java | 2 +- .../DataOfferDetailPageQueryService.java | 5 +- .../dao/pages/dataoffer/ViewCountLogger.java | 14 + .../dataoffer/model/DataOfferDetailRs.java | 3 +- .../dao/utils/JsonDeserializationUtils.java | 2 +- .../services/ConnectorCleaner.java | 2 +- .../services/ConnectorCreator.java | 3 +- .../services/OfflineConnectorKiller.java | 1 - .../services/api/CatalogApiService.java | 13 +- .../services/api/ConnectorApiService.java | 107 +----- .../api/ConnectorDetailApiService.java | 46 +++ .../services/api/ConnectorListApiService.java | 71 ++++ .../api/ConnectorOnlineStatusMapper.java | 31 ++ .../services/api/ConnectorService.java | 2 +- .../api/DataOfferDetailApiService.java | 12 +- .../services/api/DataOfferMappingUtils.java | 38 +++ .../services/api/PolicyDtoBuilder.java | 43 --- ...talogFilterAttributeDefinitionService.java | 31 +- .../api/filtering/CatalogFilterService.java | 105 +++--- .../api/filtering/CatalogSearchService.java | 39 +++ .../services/config/DataSpaceConnector.java | 1 - .../logging/ConnectorChangeTracker.java | 8 +- .../ConnectorUpdateSuccessWriter.java | 33 +- .../services/refreshing/ConnectorUpdater.java | 8 +- ...aOfferFetcher.java => CatalogFetcher.java} | 19 +- .../offers/ContractOfferFetcher.java | 46 --- .../offers/ContractOfferRecordUpdater.java | 16 +- .../refreshing/offers/DataOfferBuilder.java | 135 -------- .../offers/DataOfferPatchBuilder.java | 20 +- .../offers/DataOfferRecordUpdater.java | 153 ++++++++- .../offers/FetchedCatalogBuilder.java | 98 ++++++ .../offers/model/DataOfferPatch.java | 14 +- .../offers/model/FetchedCatalog.java | 33 ++ ...ctOffer.java => FetchedContractOffer.java} | 2 +- .../offers/model/FetchedDataOffer.java | 17 +- .../ext/brokerserver/utils/JsonUtils2.java | 32 ++ .../edc/ext/brokerserver/utils/UrlUtils.java | 23 -- .../edc/ext/brokerserver/AssertionUtils.java | 5 + .../edc/ext/brokerserver/TestAsset.java | 62 ++++ .../edc/ext/brokerserver/TestPolicy.java | 70 ++++ .../edc/ext/brokerserver/TestUtils.java | 33 +- .../edc/ext/brokerserver/db/TestDatabase.java | 2 +- .../services/api/CatalogApiTest.java | 309 +++++++++--------- .../services/api/ConnectorApiTest.java | 86 ++--- .../services/api/DataOfferCountApiTest.java | 29 +- .../services/api/DataOfferDetailApiTest.java | 110 +++---- .../services/api/DeleteConnectorsApiTest.java | 17 +- .../logging/BrokerEventLoggerTest.java | 2 +- .../refreshing/ConnectorUpdaterTest.java | 249 +++++++------- .../offers/DataOfferLimitsEnforcerTest.java | 14 +- .../offers/DataOfferWriterTest.java | 7 +- .../offers/DataOfferWriterTestDataHelper.java | 37 ++- .../offers/DataOfferWriterTestDataModels.java | 4 +- .../offers/DataOfferWriterTestDydi.java | 6 +- .../DataOfferWriterTestResultHelper.java | 12 +- .../OfflineConnectorRemovalJobTest.java | 9 +- .../brokerserver/utils/JsonUtils2Test.java | 34 ++ gradle.properties | 4 +- 92 files changed, 1784 insertions(+), 1199 deletions(-) create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/{DataOfferContractOfferQueries.java => ContractOfferQueries.java} (67%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/{ConnectorPageQueryService.java => ConnectorListQueryService.java} (60%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/{DataOfferFetcher.java => CatalogFetcher.java} (63%) delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java rename extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/{FetchedDataOfferContractOffer.java => FetchedContractOffer.java} (93%) create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 719d1b64c..28b88eec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major +- Migrated to Eclipse EDC 0.2.1 +- Migrated to edc-extensions 5.0.0 +- Migrated Assets to JSON-LD + #### Minor +- New Filter: Organization Name +- Search now hits Organization Name + #### Patch ### Deployment Migration Notes +- All connectors need to be re-crawled for detailed asset metadata and participant IDs to work + ## [v1.2.0] - 2023-10-30 ### Overview @@ -85,7 +94,7 @@ Bugfix release for the asset properties issue. Also contains the connector delet --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: ApiKeyDefaultValue' \ - --data '["https://some-connector-to-delete/api/v1/ids/data", "https://some-other-connector-to-delete/api/v1/ids/data"]' + --data '["https://some-connector-to-delete/api/dsp", "https://some-other-connector-to-delete/api/dsp"]' ``` #### Compatible Versions @@ -201,7 +210,7 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: ApiKeyDefaultValue' \ - --data '["https://some-new-connector/api/v1/ids/data", "https://some-other-new-connector/api/v1/ids/data"]' + --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' ``` #### Compatible Versions @@ -248,7 +257,7 @@ Broker MvP using Core EDC MS8. 1. There are new **required** configuration properties: ```yaml # List of Data Space Names for special Connectors (default: '') - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/ids/data,OtherDataspace=https://some-other-connector/ids/data" + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/api/dsp,OtherDataspace=https://some-other-connector/api/dsp" ``` 2. There are new **optional** configuration properties available for overriding: ```yaml diff --git a/README.md b/README.md index db26877bb..e52b7f175 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,10 @@ MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc # Required: List of EDCs to fetch -EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/ids/data,https://connector-b/ids/data" +EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/api/dsp,https://connector-b/api/dsp" # List of Data Space Names for special Connectors (default: '') -EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/ids/data,OtherDataspace=https://some-other-connector/ids/data" +EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/api/dsp,OtherDataspace=https://some-other-connector/api/dsp" # Required: DAPS credentials EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' @@ -162,7 +162,7 @@ curl --request PUT \ --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: ApiKeyDefaultValue' \ - --data '["https://some-new-connector/api/v1/ids/data", "https://some-other-new-connector/api/v1/ids/data"]' + --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' ``` #### Removing Connectors at runtime @@ -175,7 +175,7 @@ curl --request DELETE \ --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: ApiKeyDefaultValue' \ - --data '["https://some-connector-to-be-removed/api/v1/ids/data", "https://some-other-connector-to-be-removed/api/v1/ids/data"]' + --data '["https://some-connector-to-be-removed/api/dsp", "https://some-other-connector-to-be-removed/api/dsp"]' ```

(back to top)

diff --git a/build.gradle.kts b/build.gradle.kts index bd4c4bbbe..e3c232a72 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,6 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + plugins { id("java") id("checkstyle") @@ -26,8 +29,12 @@ allprojects { tasks.getByName("test") { useJUnitPlatform() testLogging { - events("passed", "skipped", "failed") + events = setOf(TestLogEvent.FAILED) + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true } + failFast = true } checkstyle { diff --git a/connector/.env b/connector/.env index 0afa888d7..587bc2e28 100644 --- a/connector/.env +++ b/connector/.env @@ -33,7 +33,7 @@ EDC_BROKER_SERVER_KNOWN_CONNECTORS= EDC_BROKER_SERVER_DEFAULT_DATASPACE=MDS # List of Data Space Names for special Connectors -# e.g. Mobilithek=https://my-connector1/ids/data,SomeOtherDataspace=https://my-connector2/ids/data +# e.g. Mobilithek=https://my-connector1/api/dsp,SomeOtherDataspace=https://my-connector2/api/dsp EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS= # CRON interval for crawling ONLINE connectors diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index d373b592a..40f54740a 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -14,7 +14,8 @@ dependencies { implementation("${edcGroup}:configuration-filesystem:${edcVersion}") implementation("${edcGroup}:control-plane-aggregate-services:${edcVersion}") implementation("${edcGroup}:http:${edcVersion}") - implementation("${edcGroup}:ids:${edcVersion}") + implementation("${edcGroup}:dsp:${edcVersion}") + implementation("${edcGroup}:json-ld:${edcVersion}") // JDK Logger implementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") diff --git a/docker-compose.yaml b/docker-compose.yaml index 6beaeab6d..39c2bc8f8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,8 +19,8 @@ services: MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/v1/ids/data" - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/ids/data" + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/dsp" + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" # Local Dev / Docker-Compose Config MY_EDC_PROTOCOL: "http://" # We don't have TLS in the docker container @@ -44,7 +44,7 @@ services: POSTGRESQL_PASSWORD: edc POSTGRESQL_DATABASE: edc ports: - - '54321:5432' + - '54321:5432' volumes: - 'broker-postgresql:/bitnami/postgresql' connector-ui: diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index c24eefe99..92be0fca4 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -65,7 +65,7 @@ public interface BrokerServerResource { @Path("connector-detail-page") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query a Known Connector's Detail Page") + @Operation(description = "Query a known Connector's Detail Page") ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query); @PUT diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java index 87cebdb94..31f4b02a9 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.api.model; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -41,6 +41,6 @@ public class CatalogContractOffer { private OffsetDateTime updatedAt; @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) - private PolicyDto contractPolicy; + private UiPolicy contractPolicy; } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java index 44bcd3782..4c89b522b 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.api.model; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -23,7 +24,6 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Map; @Getter @Setter @@ -35,7 +35,7 @@ public class CatalogDataOffer { @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) private String assetId; - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) private String connectorEndpoint; @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) @@ -51,7 +51,7 @@ public class CatalogDataOffer { private OffsetDateTime updatedAt; @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) - private Map properties; + private UiAsset asset; @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) private List contractOffers; diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java index 3928045a8..74c7c2710 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java @@ -30,10 +30,10 @@ @AllArgsConstructor @Schema(description = "Connector Detail Page Data") public class ConnectorDetailPageResult { - @Schema(description = "Connector ID", example = "https://my-test.connector", requiredMode = Schema.RequiredMode.REQUIRED) - private String id; + @Schema(description = "Connector Participant ID", example = "https://my-test.connector", requiredMode = Schema.RequiredMode.REQUIRED) + private String participantId; - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) private String endpoint; @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) @@ -49,7 +49,7 @@ public class ConnectorDetailPageResult { private ConnectorOnlineStatus onlineStatus; @Schema(description = "Number of known data offerings") - private Integer numContractOffers; + private Integer numDataOffers; @Schema(description = "Average time to crawl the connector") private Long connectorCrawlingTimeAvg; diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java index 42248d5c5..47ee2a661 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java @@ -30,10 +30,10 @@ @AllArgsConstructor @Schema(description = "A Contract Offer's Connector Status") public class ConnectorListEntry { - @Schema(description = "Connector ID", example = "https://my-test.connector", requiredMode = Schema.RequiredMode.REQUIRED) - private String id; + @Schema(description = "Connector Participant ID", example = "my-test-connector", requiredMode = Schema.RequiredMode.REQUIRED) + private String participantId; - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) private String endpoint; @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) @@ -49,6 +49,6 @@ public class ConnectorListEntry { private ConnectorOnlineStatus onlineStatus; @Schema(description = "Number of known data offerings") - private Integer numContractOffers; + private Integer numDataOffers; } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java index 6803d2293..c5554c765 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.api.model; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -41,6 +41,6 @@ public class DataOfferDetailContractOffer { private OffsetDateTime updatedAt; @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) - private PolicyDto contractPolicy; + private UiPolicy contractPolicy; } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java index bc40b36ea..354a63c81 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.api.model; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -23,7 +24,6 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Map; @Getter @Setter @@ -35,7 +35,7 @@ public class DataOfferDetailPageResult { @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) private String assetId; - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/control/ids/data", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) private String connectorEndpoint; @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) @@ -51,7 +51,7 @@ public class DataOfferDetailPageResult { private OffsetDateTime updatedAt; @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) - private Map properties; + private UiAsset asset; @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) private List contractOffers; diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index a787bffee..12b88dd30 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -46,13 +46,8 @@ dependencies { implementation("org.apache.commons:commons-lang3:3.13.0") implementation("${edcGroup}:core-spi:${edcVersion}") - implementation("${edcGroup}:sql-core:${edcVersion}") // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) - implementation("${edcGroup}:control-plane-sql:${edcVersion}") - implementation("${edcGroup}:data-plane-instance-store-sql:${edcVersion}") - implementation("${edcGroup}:sql-pool-apache-commons:${edcVersion}") - implementation("${edcGroup}:transaction-local:${edcVersion}") implementation("org.postgresql:postgresql:${postgresVersion}") api("org.flywaydb:flyway-core:${flywayVersion}") diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java index f4ec17d7e..0dcf2058a 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java @@ -14,9 +14,6 @@ package de.sovity.edc.ext.brokerserver.db; -import org.eclipse.edc.connector.dataplane.selector.store.sql.schema.DataPlaneInstanceStatements; -import org.eclipse.edc.connector.dataplane.selector.store.sql.schema.postgres.PostgresDataPlaneInstanceStatements; -import org.eclipse.edc.runtime.metamodel.annotation.Provider; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; @@ -39,11 +36,6 @@ public class PostgresFlywayExtension implements ServiceExtension { @Setting public static final String DB_CONNECTION_TIMEOUT_IN_MS = "edc.broker.server.db.connection.timeout.in.ms"; - @Provider - public DataPlaneInstanceStatements dataPlaneInstanceStatements() { - return new PostgresDataPlaneInstanceStatements(); - } - @Override public String name() { return "Postgres Flyway Extension (Broker Server)"; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql new file mode 100644 index 000000000..c8edbe6fc --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql @@ -0,0 +1,166 @@ +-- Migration Script for Broker from MS8 to EDC 0 + +-- Migrates an Asset ID +create + or replace function pg_temp.migrate_asset_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:artifact:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Migrates a Connector Endpoint to EDC 0 +create + or replace function pg_temp.migrate_connector_endpoint(endpoint text) returns text as +$$ +begin + return pg_temp.replace_suffix(endpoint, '/api/v1/ids/data', '/api/dsp'); +end; +$$ + language plpgsql; + +-- Creates a valid Asset JSON-LD from an Asset ID and Asset Title +create + or replace function pg_temp.build_asset_json_ld(asset_id text, asset_title text) returns jsonb as +$$ +begin + return jsonb_build_object( + '@id', asset_id, + 'https://w3id.org/edc/v0.0.1/ns/properties', jsonb_build_object( + 'https://w3id.org/edc/v0.0.1/ns/id', asset_id, + 'http://purl.org/dc/terms/title', asset_title + ) + ); +end; +$$ + language plpgsql; + +-- Utility Function: replaceSuffix +create + or replace function pg_temp.replace_suffix(str text, old_suffix text, new_suffix text) returns text as +$$ +begin + return case + when pg_temp.ends_with(str, old_suffix) then + left(str, length(str) - length(old_suffix)) || new_suffix + else + str + end; +end; +$$ + language plpgsql; + +-- Utility Function: endsWith +create or replace function pg_temp.ends_with(str text, suffix text) + returns boolean as +$$ +begin + return right(str, length(suffix)) = suffix; +end; +$$ language plpgsql; + +-- Utility Function: Drops fkey constraints that have auto-generated names. Different Postgresql versions generated different names. +create or replace function pg_temp.drop_constraints_containing_fkey(table_name text) + returns void as +$$ +declare + i record; +begin + for i in (select conname + from pg_catalog.pg_constraint con + inner join pg_catalog.pg_class rel on rel.oid = con.conrelid + inner join pg_catalog.pg_namespace nsp on nsp.oid = connamespace + where rel.relname = table_name + and conname like '%fkey%') + loop + execute format('alter table %s drop constraint %s', table_name, i.conname); + end loop; +end; +$$ language plpgsql; + + +-- Remove Connector Tables +-- All connector tables should be empty +-- There should be no references from broker tables to connector tables +drop table edc_asset cascade; +drop table edc_asset_dataaddress cascade; +drop table edc_asset_property cascade; +drop table edc_contract_agreement cascade; +drop table edc_contract_definitions cascade; +drop table edc_contract_negotiation cascade; +drop table edc_data_plane_instance cascade; +drop table edc_data_request cascade; +drop table edc_lease cascade; +drop table edc_policydefinitions cascade; +drop table edc_transfer_process cascade; + + +-- Drop constraints +select pg_temp.drop_constraints_containing_fkey('data_offer'); +select pg_temp.drop_constraints_containing_fkey('data_offer_contract_offer'); + +-- Migrate Connector Endpoints +update broker_event_log +set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); +update broker_execution_time_measurement +set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); +update connector +set endpoint = pg_temp.migrate_connector_endpoint(endpoint); +update data_offer +set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); +update data_offer_contract_offer +set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); +update data_offer_view_count +set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); + + +-- Migrate Asset IDs +update broker_event_log +set asset_id = pg_temp.migrate_asset_id(asset_id); +update data_offer +set asset_id = pg_temp.migrate_asset_id(asset_id); +update data_offer_contract_offer +set asset_id = pg_temp.migrate_asset_id(asset_id); +update data_offer_view_count +set asset_id = pg_temp.migrate_asset_id(asset_id); + +-- Rename data_offer_contract_offer to contract_offer +alter table data_offer_contract_offer + rename to contract_offer; + +-- Rename Connector ID to Participant ID +alter table connector + rename column connector_id to participant_id; + +-- Add constraints +alter table data_offer + add constraint data_offer_connector_endpoint_fkey + foreign key (connector_endpoint) references connector (endpoint); +alter table contract_offer + add constraint contract_offer_data_offer_fkey + foreign key (connector_endpoint, asset_id) references data_offer (connector_endpoint, asset_id); +alter table contract_offer + add constraint contract_offer_connector_fkey + foreign key (connector_endpoint) references connector (endpoint); + +-- Migrate to Asset JSON-LD +alter table data_offer + rename column asset_properties to asset_json_ld; +alter table data_offer + rename column asset_name to asset_title; +update data_offer +set asset_json_ld = pg_temp.build_asset_json_ld(asset_id, asset_title); + +-- Extracted Asset Metadata from the JSON-LD for Search / Filtering +alter table data_offer + add column description text not null default '', + add column curator_organization_name text not null default '', + add column data_category text not null default '', + add column data_subcategory text not null default '', + add column data_model text not null default '', + add column transport_mode text not null default '', + add column geo_reference_method text not null default '', + add column keywords text[] not null default '{}', + -- comma joined keywords for easier search + add column keywords_comma_joined text not null default ''; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql index b3235f637..fd42a12dd 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql @@ -2,23 +2,23 @@ insert into connector (endpoint, connector_id, created_at, last_refresh_attempt_at, last_successful_refresh_at, online_status) -values ('https://my-connector.com/ids/data', 'test-connector-1', '2019-01-01 00:00:00', +values ('https://my-connector.com/api/v1/ids/data', 'test-connector-1', '2019-01-01 00:00:00', '2019-01-01 00:00:00', '2019-01-01 00:00:00', 'ONLINE'); insert into data_offer (connector_endpoint, asset_id, asset_properties, created_at, updated_at) -values ('https://my-connector.com/ids/data', +values ('https://my-connector.com/api/v1/ids/data', 'test-asset-1', '{ "asset:prop:id": "test-asset-1" }', '2019-01-01 00:00:00', '2019-01-01 00:00:00'), - ('https://my-connector.com/ids/data', + ('https://my-connector.com/api/v1/ids/data', 'test-asset-2', '{ "asset:prop:id": "urn:artifact:db-rail-network-2023-jan", "asset:prop:name": "Rail Network DB 2023 January", "asset:prop:version": "1.1", - "asset:prop:originator": "https://example-connector.rail-mgmt.bahn.de/api/v1/ids/data", + "asset:prop:originator": "https://example-connector.rail-mgmt.bahn.de/api/v1/api/v1/ids/data", "asset:prop:originatorOrganization": "Deutsche Bahn AG", "asset:prop:keywords": "db, bahn, rail, Rail-Designer", "asset:prop:contenttype": "application/json", @@ -38,13 +38,13 @@ values ('https://my-connector.com/ids/data', insert into data_offer_contract_offer (contract_offer_id, connector_endpoint, asset_id, policy, created_at, updated_at) values ('test-contract-offer-1', - 'https://my-connector.com/ids/data', + 'https://my-connector.com/api/v1/ids/data', 'test-asset-1', '"test-policy-1"', '2019-01-01 00:00:00', '2019-01-01 00:00:00'), ('test-contract-offer-2', - 'https://my-connector.com/ids/data', + 'https://my-connector.com/api/v1/ids/data', 'test-asset-2', '"test-policy-2"', '2019-01-01 00:00:00', @@ -56,13 +56,13 @@ values ('2019-01-01 00:00:00', 'Connector was successfully updated, and changes were incorporated', 'CONNECTOR_UPDATED', 'OK', - 'https://my-connector.com/ids/data', + 'https://my-connector.com/api/v1/ids/data', 'test-asset-1', null, 100); insert into broker_execution_time_measurement (connector_endpoint, created_at, type, error_status, duration_in_ms) -values ('https://my-connector.com/ids/data', +values ('https://my-connector.com/api/v1/ids/data', '2019-01-01 00:00:00', 'CONNECTOR_REFRESH', 'OK', diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index aec9ba7b1..0e92eb5e6 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -11,6 +11,7 @@ val okHttpVersion: String by project val restAssured: String by project val testcontainersVersion: String by project val sovityEdcGroup: String by project +val sovityEdcExtensionGroup: String by project val sovityEdcExtensionsVersion: String by project configurations.all { @@ -22,6 +23,10 @@ dependencies { compileOnly("org.projectlombok:lombok:1.18.30") implementation("org.apache.commons:commons-lang3:3.13.0") + api("${sovityEdcGroup}:catalog-parser:${sovityEdcExtensionsVersion}") { isChanging = true } + api("${sovityEdcGroup}:json-and-jsonld-utils:${sovityEdcExtensionsVersion}") { isChanging = true } + api("${sovityEdcGroup}:wrapper-common-mappers:${sovityEdcExtensionsVersion}") { isChanging = true } + implementation("${edcGroup}:control-plane-spi:${edcVersion}") implementation("${edcGroup}:management-api-configuration:${edcVersion}") @@ -32,14 +37,18 @@ dependencies { testAnnotationProcessor("org.projectlombok:lombok:1.18.30") testCompileOnly("org.projectlombok:lombok:1.18.30") + testImplementation("${sovityEdcGroup}:client:${sovityEdcExtensionsVersion}") { isChanging = true } + testImplementation("${sovityEdcExtensionGroup}:sovity-edc-extensions-package:${sovityEdcExtensionsVersion}") { isChanging = true } testImplementation("org.assertj:assertj-core:${assertj}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("org.mockito:mockito-inline:${mockitoVersion}") testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") testImplementation("${edcGroup}:junit:${edcVersion}") testImplementation("${edcGroup}:http:${edcVersion}") testImplementation("${edcGroup}:iam-mock:${edcVersion}") - testImplementation("${edcGroup}:ids:${edcVersion}") + testImplementation("${edcGroup}:dsp:${edcVersion}") + testImplementation("${edcGroup}:json-ld:${edcVersion}") testImplementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") testImplementation("${edcGroup}:configuration-filesystem:${edcVersion}") testImplementation(project(":extensions:broker-server-api:client")) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java index 65c61f688..3b288ea13 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java @@ -15,7 +15,9 @@ package de.sovity.edc.ext.brokerserver; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; @@ -80,6 +82,12 @@ public class BrokerServerExtension implements ServiceExtension { @Inject private TypeManager typeManager; + @Inject + private ManagementApiTypeTransformerRegistry typeTransformerRegistry; + + @Inject + private JsonLd jsonLd; + @Inject private CatalogService catalogService; @@ -99,6 +107,8 @@ public void initialize(ServiceExtensionContext context) { context.getConfig(), context.getMonitor(), typeManager, + typeTransformerRegistry, + jsonLd, catalogService ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java index f9010ec1d..3dd8bd2fa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java @@ -18,6 +18,9 @@ import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.FetchedCatalogBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; /** @@ -32,7 +35,10 @@ public record BrokerServerExtensionContext( // Required for Integration Tests ConnectorUpdater connectorUpdater, - ConnectorCreator connectorCreator + ConnectorCreator connectorCreator, + PolicyMapper policyMapper, + FetchedCatalogBuilder fetchedCatalogBuilder, + DataOfferRecordUpdater dataOfferRecordUpdater ) { /** * This is a hack for our tests. diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 330c9f53b..89b7477b9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -14,8 +14,11 @@ package de.sovity.edc.ext.brokerserver; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.dao.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.ContractOfferQueries; import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryAvailableFilterFetcher; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryContractOfferFetcher; @@ -23,7 +26,8 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFilterService; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQuerySortingService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorDetailQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorListQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.ViewCountLogger; import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; @@ -34,16 +38,19 @@ import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; -import de.sovity.edc.ext.brokerserver.services.api.AssetPropertyParser; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorListApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorOnlineStatusMapper; import de.sovity.edc.ext.brokerserver.services.api.ConnectorService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferCountApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; +import de.sovity.edc.ext.brokerserver.services.api.DataOfferMappingUtils; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; -import de.sovity.edc.ext.brokerserver.services.api.PolicyDtoBuilder; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterAttributeDefinitionService; import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; +import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogSearchService; import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettingsFactory; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; @@ -55,27 +62,41 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.CatalogFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferRecordUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferBuilder; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchApplier; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchBuilder; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.FetchedCatalogBuilder; import de.sovity.edc.ext.brokerserver.services.schedules.DeadConnectorRefreshJob; import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorKillerJob; import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorRefreshJob; import de.sovity.edc.ext.brokerserver.services.schedules.OnlineConnectorRefreshJob; import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.utils.catalog.DspCatalogService; +import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; import lombok.NoArgsConstructor; import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.CoreConstants; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -96,6 +117,8 @@ public static BrokerServerExtensionContext buildContext( Config config, Monitor monitor, TypeManager typeManager, + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd, CatalogService catalogService ) { var brokerServerSettingsFactory = new BrokerServerSettingsFactory(config, monitor); @@ -109,7 +132,8 @@ public static BrokerServerExtensionContext buildContext( var dslContextFactory = new DslContextFactory(dataSource); var connectorQueries = new ConnectorQueries(); var catalogQuerySortingService = new CatalogQuerySortingService(); - var catalogQueryFilterService = new CatalogQueryFilterService(brokerServerSettings); + var catalogSearchService = new CatalogSearchService(); + var catalogQueryFilterService = new CatalogQueryFilterService(brokerServerSettings, catalogSearchService); var catalogQueryContractOfferFetcher = new CatalogQueryContractOfferFetcher(); var catalogQueryDataOfferFetcher = new CatalogQueryDataOfferFetcher( catalogQuerySortingService, @@ -122,21 +146,22 @@ public static BrokerServerExtensionContext buildContext( catalogQueryAvailableFilterFetcher, brokerServerSettings ); - var connectorPageQueryService = new ConnectorPageQueryService(); + var connectorListQueryService = new ConnectorListQueryService(); + var connectorDetailQueryService = new ConnectorDetailQueryService(); var dataOfferDetailPageQueryService = new DataOfferDetailPageQueryService( catalogQueryContractOfferFetcher, brokerServerSettings); // Services - var objectMapper = typeManager.getMapper(); + var objectMapperJsonLd = getJsonLdObjectMapper(typeManager); var brokerEventLogger = new BrokerEventLogger(); var brokerExecutionTimeLogger = new BrokerExecutionTimeLogger(); var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); var dataOfferRecordUpdater = new DataOfferRecordUpdater(); - var dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); + var contractOfferQueries = new ContractOfferQueries(); var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(brokerServerSettings, brokerEventLogger); var dataOfferPatchBuilder = new DataOfferPatchBuilder( - dataOfferContractOfferQueries, + contractOfferQueries, dataOfferQueries, dataOfferRecordUpdater, contractOfferRecordUpdater @@ -148,10 +173,25 @@ public static BrokerServerExtensionContext buildContext( dataOfferWriter, dataOfferLimitsEnforcer ); + var edcPropertyUtils = new EdcPropertyUtils(); + var assetJsonLdUtils = new AssetJsonLdUtils(); + var uiAssetMapper = new UiAssetMapper( + edcPropertyUtils, + assetJsonLdUtils + ); + var assetMapper = new AssetMapper( + typeTransformerRegistry, + uiAssetMapper, + jsonLd + ); + var fetchedDataOfferBuilder = new FetchedCatalogBuilder(assetMapper); + var dspDataOfferBuilder = new DspDataOfferBuilder(jsonLd); + var dspCatalogService = new DspCatalogService( + catalogService, + dspDataOfferBuilder + ); + var dataOfferFetcher = new CatalogFetcher(dspCatalogService, fetchedDataOfferBuilder); var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger, monitor); - var contractOfferFetcher = new ContractOfferFetcher(catalogService); - var fetchedDataOfferBuilder = new DataOfferBuilder(objectMapper); - var dataOfferFetcher = new DataOfferFetcher(contractOfferFetcher, fetchedDataOfferBuilder); var connectorUpdater = new ConnectorUpdater( dataOfferFetcher, connectorUpdateSuccessWriter, @@ -161,8 +201,6 @@ public static BrokerServerExtensionContext buildContext( monitor, brokerExecutionTimeLogger ); - var policyDtoBuilder = new PolicyDtoBuilder(); - var assetPropertyParser = new AssetPropertyParser(objectMapper); var paginationMetadataUtils = new PaginationMetadataUtils(); var threadPoolTaskQueue = new ThreadPoolTaskQueue(); var threadPool = new ThreadPool(threadPoolTaskQueue, brokerServerSettings, monitor); @@ -187,6 +225,29 @@ public static BrokerServerExtensionContext buildContext( connectorKiller, connectorClearer ); + var operatorMapper = new OperatorMapper(); + var literalMapper = new LiteralMapper( + objectMapperJsonLd + ); + var atomicConstraintMapper = new AtomicConstraintMapper( + literalMapper, + operatorMapper + ); + var policyValidator = new PolicyValidator(); + var constraintExtractor = new ConstraintExtractor( + policyValidator, + atomicConstraintMapper + ); + var policyMapper = new PolicyMapper( + constraintExtractor, + atomicConstraintMapper, + typeTransformerRegistry + ); + var dataOfferMappingUtils = new DataOfferMappingUtils( + policyMapper, + assetMapper + ); + var connectorOnlineStatusMapper = new ConnectorOnlineStatusMapper(); // Schedules List> jobs = List.of( @@ -208,27 +269,27 @@ public static BrokerServerExtensionContext buildContext( var catalogApiService = new CatalogApiService( paginationMetadataUtils, catalogQueryService, - policyDtoBuilder, - assetPropertyParser, + dataOfferMappingUtils, catalogFilterService, brokerServerSettings ); var connectorApiService = new ConnectorApiService( - connectorPageQueryService, connectorService, - paginationMetadataUtils, brokerEventLogger ); var dataOfferDetailApiService = new DataOfferDetailApiService( dataOfferDetailPageQueryService, viewCountLogger, - policyDtoBuilder, - assetPropertyParser + dataOfferMappingUtils ); var dataOfferCountApiService = new DataOfferCountApiService(); + var connectorDetailApiService = new ConnectorDetailApiService(connectorDetailQueryService, connectorOnlineStatusMapper); + var connectorListApiService = new ConnectorListApiService(connectorListQueryService, connectorOnlineStatusMapper, paginationMetadataUtils); var brokerServerResource = new BrokerServerResourceImpl( dslContextFactory, connectorApiService, + connectorListApiService, + connectorDetailApiService, catalogApiService, dataOfferDetailApiService, adminApiKeyValidator, @@ -239,7 +300,10 @@ public static BrokerServerExtensionContext buildContext( brokerServerResource, brokerServerInitializer, connectorUpdater, - connectorCreator + connectorCreator, + policyMapper, + fetchedDataOfferBuilder, + dataOfferRecordUpdater ); } @@ -278,4 +342,15 @@ private static CronJobRef getDeadConnectorRefreshCronJo () -> new DeadConnectorRefreshJob(dslContextFactory, connectorQueueFiller) ); } + + private static ObjectMapper getJsonLdObjectMapper(TypeManager typeManager) { + var objectMapper = typeManager.getMapper(CoreConstants.JSON_LD); + + // Fixes Dates in JSON-LD Object Mapper + // The Core EDC uses longs over OffsetDateTime, so they never fixed the date format + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + return objectMapper; + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index eb34766e1..32fc227ff 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -27,6 +27,8 @@ import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; +import de.sovity.edc.ext.brokerserver.services.api.ConnectorListApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferCountApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; @@ -42,6 +44,8 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final DslContextFactory dslContextFactory; private final ConnectorApiService connectorApiService; + private final ConnectorListApiService connectorListApiService; + private final ConnectorDetailApiService connectorDetailApiService; private final CatalogApiService catalogApiService; private final DataOfferDetailApiService dataOfferDetailApiService; private final AdminApiKeyValidator adminApiKeyValidator; @@ -54,7 +58,7 @@ public CatalogPageResult catalogPage(CatalogPageQuery query) { @Override public ConnectorPageResult connectorPage(ConnectorPageQuery query) { - return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorPage(dsl, query)); + return dslContextFactory.transactionResult(dsl -> connectorListApiService.connectorListPage(dsl, query)); } @Override @@ -64,7 +68,7 @@ public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery qu @Override public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query) { - return dslContextFactory.transactionResult(dsl -> connectorApiService.connectorDetailPage(dsl, query)); + return dslContextFactory.transactionResult(dsl -> connectorDetailApiService.connectorDetailPage(dsl, query)); } @Override diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java deleted file mode 100644 index 9d24f3bf2..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/AssetProperty.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao; - - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class AssetProperty { - public static final String ASSET_ID = "asset:prop:id"; - public static final String ASSET_NAME = "asset:prop:name"; - public static final String DESCRIPTION = "asset:prop:description"; - public static final String KEYWORDS = "asset:prop:keywords"; - - - public static final String CONTENT_TYPE = "asset:prop:contenttype"; - public static final String ORIGINATOR = "asset:prop:originator"; - public static final String ORIGINATOR_ORGANIZATION = "asset:prop:originatorOrganization"; - public static final String VERSION = "asset:prop:version"; - public static final String CURATOR_ORGANIZATION_NAME = "asset:prop:curatorOrganizationName"; - public static final String LANGUAGE = "asset:prop:language"; - public static final String PUBLISHER = "asset:prop:publisher"; - public static final String STANDARD_LICENSE = "asset:prop:standardLicense"; - public static final String ENDPOINT_DOCUMENTATION = "asset:prop:endpointDocumentation"; - - public static final String DATA_CATEGORY = "http://w3id.org/mds#dataCategory"; - public static final String DATA_SUBCATEGORY = "http://w3id.org/mds#dataSubcategory"; - public static final String DATA_MODEL = "http://w3id.org/mds#dataModel"; - public static final String GEO_REFERENCE_METHOD = "http://w3id.org/mds#geoReferenceMethod"; - public static final String TRANSPORT_MODE = "http://w3id.org/mds#transportMode"; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferContractOfferQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ContractOfferQueries.java similarity index 67% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferContractOfferQueries.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ContractOfferQueries.java index c02d0d52c..be5ef734c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferContractOfferQueries.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ContractOfferQueries.java @@ -15,15 +15,15 @@ package de.sovity.edc.ext.brokerserver.dao; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; import org.jooq.DSLContext; import java.util.List; -public class DataOfferContractOfferQueries { +public class ContractOfferQueries { - public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { - var co = Tables.DATA_OFFER_CONTRACT_OFFER; + public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { + var co = Tables.CONTRACT_OFFER; return dsl.selectFrom(co).where(co.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java index 564f20a38..ee3fdd7aa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java @@ -68,7 +68,7 @@ private Field queryFilterValues( return DSL.select(DSL.coalesce(DSL.arrayAggDistinct(value), DSL.array().cast(SQLDataType.VARCHAR.array()))) .from(d) .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) - .where(catalogQueryFilterService.filter(fields, searchQuery, otherFilters)) + .where(catalogQueryFilterService.filterDbQuery(fields, searchQuery, otherFilters)) .asField(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java index 364c7683b..d2e370c5c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java @@ -17,6 +17,7 @@ import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; import lombok.RequiredArgsConstructor; import org.jooq.Field; import org.jooq.impl.DSL; @@ -29,12 +30,11 @@ public class CatalogQueryContractOfferFetcher { /** * Query a data offer's contract offers. * - * @param fields query fields + * @param d Data offer table * @return {@link Field} of {@link ContractOfferRs}s */ - public Field> getContractOffers(CatalogQueryFields fields) { - var d = fields.getDataOfferTable(); - var co = Tables.DATA_OFFER_CONTRACT_OFFER; + public Field> getContractOffers(DataOffer d) { + var co = Tables.CONTRACT_OFFER; var query = DSL.select( co.CONTRACT_OFFER_ID, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java index 771ad908d..8c98af7b2 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -55,18 +55,19 @@ public Field> queryDataOffers( var d = fields.getDataOfferTable(); var select = DSL.select( - fields.getAssetId().as("assetId"), - d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), + d.ASSET_ID.as("assetId"), + d.ASSET_JSON_LD.cast(String.class).as("assetJsonLd"), d.CREATED_AT, d.UPDATED_AT, - catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), + catalogQueryContractOfferFetcher.getContractOffers(d).as("contractOffers"), c.ENDPOINT.as("connectorEndpoint"), c.ONLINE_STATUS.as("connectorOnlineStatus"), + c.PARTICIPANT_ID.as("connectorParticipantId"), fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt") ); var query = from(select, fields) - .where(catalogQueryFilterService.filter(fields, searchQuery, filters)) + .where(catalogQueryFilterService.filterDbQuery(fields, searchQuery, filters)) .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)) .limit(pageQuery.offset(), pageQuery.limit()); @@ -83,7 +84,7 @@ public Field> queryDataOffers( */ public Field queryNumDataOffers(CatalogQueryFields fields, String searchQuery, List filters) { var query = from(DSL.select(DSL.count()), fields) - .where(catalogQueryFilterService.filter(fields, searchQuery, filters)); + .where(catalogQueryFilterService.filterDbQuery(fields, searchQuery, filters)); return DSL.field(query); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index 7f255b146..f9dc09795 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -14,8 +14,6 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; -import com.github.t9t.jooq.json.JsonbDSL; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOfferViewCount; @@ -42,10 +40,6 @@ public class CatalogQueryFields { DataOfferViewCount dataOfferViewCountTable; // Asset Properties from JSON to be used in sorting / filtering - Field assetId; - Field assetName; - Field assetDescription; - Field assetKeywords; Field dataSpace; // This date should always be non-null @@ -64,10 +58,6 @@ public CatalogQueryFields( this.dataOfferTable = dataOfferTable; this.dataOfferViewCountTable = dataOfferViewCountTable; this.dataSpaceConfig = dataSpaceConfig; - assetId = dataOfferTable.ASSET_ID; - assetName = dataOfferTable.ASSET_NAME; - assetDescription = getAssetProperty(AssetProperty.DESCRIPTION); - assetKeywords = getAssetProperty(AssetProperty.KEYWORDS); offlineSinceOrLastUpdatedAt = DSL.coalesce( connectorTable.LAST_SUCCESSFUL_REFRESH_AT, connectorTable.CREATED_AT @@ -94,10 +84,6 @@ private Field buildDataSpaceField(Connector connectorTable, DataSpaceCon return dspCase.else_(DSL.val(dataSpaceConfig.defaultDataSpace())); } - public Field getAssetProperty(String name) { - return JsonbDSL.fieldByKeyText(dataOfferTable.ASSET_PROPERTIES, name); - } - public CatalogQueryFields withSuffix(String additionalSuffix) { return new CatalogQueryFields( connectorTable.as(withSuffix(connectorTable, additionalSuffix)), diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java index 9f83645be..b92af8920 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java @@ -14,11 +14,10 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogSearchService; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; @@ -33,18 +32,11 @@ @RequiredArgsConstructor public class CatalogQueryFilterService { private final BrokerServerSettings brokerServerSettings; + private final CatalogSearchService catalogSearchService; - public Condition filter(CatalogQueryFields fields, String searchQuery, List filters) { + public Condition filterDbQuery(CatalogQueryFields fields, String searchQuery, List filters) { var conditions = new ArrayList(); - conditions.add(SearchUtils.simpleSearch(searchQuery, List.of( - fields.getAssetId(), - fields.getAssetName(), - fields.getAssetProperty(AssetProperty.DATA_CATEGORY), - fields.getAssetProperty(AssetProperty.DATA_SUBCATEGORY), - fields.getAssetDescription(), - fields.getAssetKeywords(), - fields.getConnectorTable().ENDPOINT - ))); + conditions.add(catalogSearchService.filterBySearch(fields, searchQuery)); conditions.add(onlyOnlineOrRecentlyOfflineConnectors(fields.getConnectorTable())); conditions.addAll(filters.stream().map(CatalogQueryFilter::queryFilterClauseOrNull) .filter(Objects::nonNull).map(it -> it.filterDataOffers(fields)).toList()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java index b643d0f57..e08fdfe46 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java @@ -28,7 +28,7 @@ public List> getOrderBy(CatalogQueryFields fields, CatalogPageSort List> orderBy; if (sorting == null || sorting == CatalogPageSortingType.TITLE) { orderBy = List.of( - fields.getAssetName().asc(), + fields.getDataOfferTable().ASSET_TITLE.asc(), fields.getConnectorTable().ENDPOINT.asc() ); } else if (sorting == CatalogPageSortingType.MOST_RECENT) { @@ -39,7 +39,7 @@ public List> getOrderBy(CatalogQueryFields fields, CatalogPageSort } else if (sorting == CatalogPageSortingType.ORIGINATOR) { orderBy = List.of( fields.getConnectorTable().ENDPOINT.asc(), - fields.getAssetName().asc() + fields.getDataOfferTable().ASSET_TITLE.asc() ); } else if (sorting == CatalogPageSortingType.VIEW_COUNT) { orderBy = List.of( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java index 0abc3749a..8d177cb2e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java @@ -29,11 +29,12 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class DataOfferListEntryRs { String assetId; - String assetPropertiesJson; + String assetJsonLd; OffsetDateTime createdAt; OffsetDateTime updatedAt; List contractOffers; String connectorEndpoint; ConnectorOnlineStatus connectorOnlineStatus; + String connectorParticipantId; OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java new file mode 100644 index 000000000..336256464 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.dao.pages.connector; + +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.math.BigDecimal; + +public class ConnectorDetailQueryService { + public ConnectorDetailsRs queryConnectorDetailPage(DSLContext dsl, String connectorEndpoint) { + var c = Tables.CONNECTOR; + + return dsl.select( + c.ENDPOINT.as("endpoint"), + c.PARTICIPANT_ID.as("participantId"), + c.CREATED_AT.as("createdAt"), + c.LAST_SUCCESSFUL_REFRESH_AT.as("lastSuccessfulRefreshAt"), + c.LAST_REFRESH_ATTEMPT_AT.as("lastRefreshAttemptAt"), + c.ONLINE_STATUS.as("onlineStatus"), + dataOfferCount(c.ENDPOINT).as("numDataOffers"), + getAvgSuccessfulCrawlTimeInMs(c).as("connectorCrawlingTimeAvg")) + .from(c) + .where(c.ENDPOINT.eq(connectorEndpoint)) + .groupBy(c.ENDPOINT) + .fetchOneInto(ConnectorDetailsRs.class); + } + + @NotNull + private Field getAvgSuccessfulCrawlTimeInMs(Connector c) { + var betm = Tables.BROKER_EXECUTION_TIME_MEASUREMENT; + return DSL.select(DSL.avg(betm.DURATION_IN_MS)) + .from(betm) + .where(betm.CONNECTOR_ENDPOINT.eq(c.ENDPOINT), betm.ERROR_STATUS.eq(MeasurementErrorStatus.OK)) + .asField(); + } + + private Field dataOfferCount(Field endpoint) { + var d = Tables.DATA_OFFER; + return DSL.select(DSL.count()).from(d).where(d.CONNECTOR_ENDPOINT.eq(endpoint)).asField(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java similarity index 60% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java index 15b91fb3b..18f860f71 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java @@ -15,12 +15,10 @@ package de.sovity.edc.ext.brokerserver.dao.pages.connector; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -30,36 +28,24 @@ import java.util.List; -public class ConnectorPageQueryService { +public class ConnectorListQueryService { public List queryConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; - var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of(c.ENDPOINT, c.CONNECTOR_ID)); + var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of(c.ENDPOINT, c.PARTICIPANT_ID)); - return dsl.select(c.asterisk(), dataOfferCount(c.ENDPOINT).as("numDataOffers")) - .from(c) - .where(filterBySearchQuery) - .orderBy(sortConnectorPage(c, sorting)) - .fetchInto(ConnectorListEntryRs.class); - } - - public ConnectorDetailsRs queryConnectorDetailPage(DSLContext dsl, String connectorEndpoint) { - var c = Tables.CONNECTOR; - var betm = Tables.BROKER_EXECUTION_TIME_MEASUREMENT; - - var filterBySearchQuery = SearchUtils.simpleSearch(connectorEndpoint, List.of(c.ENDPOINT, c.CONNECTOR_ID)); - - var avgSuccessfulCrawlTimeInMs = dsl.select(DSL.avg(betm.DURATION_IN_MS)) - .from(betm) - .where(betm.CONNECTOR_ENDPOINT.eq(connectorEndpoint), betm.ERROR_STATUS.eq(MeasurementErrorStatus.OK)) - .asField(); - - return dsl.select(c.asterisk(), - dataOfferCount(c.ENDPOINT).as("numDataOffers"), - avgSuccessfulCrawlTimeInMs.as("connectorCrawlingTimeAvg")) - .from(c) - .where(filterBySearchQuery) - .groupBy(c.ENDPOINT) - .fetchOneInto(ConnectorDetailsRs.class); + return dsl.select( + c.ENDPOINT.as("endpoint"), + c.PARTICIPANT_ID.as("participantId"), + c.CREATED_AT.as("createdAt"), + c.LAST_SUCCESSFUL_REFRESH_AT.as("lastSuccessfulRefreshAt"), + c.LAST_REFRESH_ATTEMPT_AT.as("lastRefreshAttemptAt"), + c.ONLINE_STATUS.as("onlineStatus"), + dataOfferCount(c.ENDPOINT).as("numDataOffers") + ) + .from(c) + .where(filterBySearchQuery) + .orderBy(sortConnectorPage(c, sorting)) + .fetchInto(ConnectorListEntryRs.class); } @NotNull @@ -73,7 +59,7 @@ private List> sortConnectorPage(Connector c, ConnectorPageSortingT .asc(); if (sorting == null || sorting == ConnectorPageSortingType.ONLINE_STATUS) { - return List.of(onlineStatus, alphabetically); + return List.of(onlineStatus, alphabetically); } else if (sorting == ConnectorPageSortingType.TITLE) { return List.of(alphabetically, recentFirst); } else if (sorting == ConnectorPageSortingType.MOST_RECENT) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java index 39d291c68..a593dcece 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java @@ -27,7 +27,7 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class ConnectorDetailsRs { String endpoint; - String connectorId; + String participantId; OffsetDateTime createdAt; OffsetDateTime lastSuccessfulRefreshAt; OffsetDateTime lastRefreshAttemptAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java index af82420a1..189f01c26 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java @@ -27,7 +27,7 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class ConnectorListEntryRs { String endpoint; - String connectorId; + String participantId; OffsetDateTime createdAt; OffsetDateTime lastSuccessfulRefreshAt; OffsetDateTime lastRefreshAttemptAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java index fcfd96e6a..1ae809722 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java @@ -41,13 +41,14 @@ public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetI return dsl.select( d.ASSET_ID, - d.ASSET_PROPERTIES.cast(String.class).as("assetPropertiesJson"), + d.ASSET_JSON_LD.cast(String.class).as("assetJsonLd"), d.CREATED_AT, d.UPDATED_AT, - catalogQueryContractOfferFetcher.getContractOffers(fields).as("contractOffers"), + catalogQueryContractOfferFetcher.getContractOffers(fields.getDataOfferTable()).as("contractOffers"), fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt"), c.ENDPOINT.as("connectorEndpoint"), c.ONLINE_STATUS.as("connectorOnlineStatus"), + c.PARTICIPANT_ID.as("connectorParticipantId"), fields.getViewCount().as("viewCount")) .from(d) .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java index 7fedc8f4c..377abeafa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java index 4d2cb5241..54953edb8 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java @@ -28,12 +28,13 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class DataOfferDetailRs { String assetId; - String assetPropertiesJson; + String assetJsonLd; OffsetDateTime createdAt; OffsetDateTime updatedAt; List contractOffers; String connectorEndpoint; ConnectorOnlineStatus connectorOnlineStatus; + String connectorParticipantId; OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; Integer viewCount; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java index 3803c91fe..1c3f4e2f9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java @@ -33,7 +33,7 @@ public class JsonDeserializationUtils { }; @SneakyThrows - public static List> deserializeStringArray2(String json) { + public static List> read2dStringList(String json) { return OBJECT_MAPPER.readValue(json, TYPE_STRING_LIST_2); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java index 512fdbd6e..b21cc2787 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java @@ -22,7 +22,7 @@ public class ConnectorCleaner { public void removeDataForDeadConnectors(DSLContext dsl, Collection endpoints) { - var doco = Tables.DATA_OFFER_CONTRACT_OFFER; + var doco = Tables.CONTRACT_OFFER; var dof = Tables.DATA_OFFER; dsl.deleteFrom(doco).where(PostgresqlUtils.in(doco.CONNECTOR_ENDPOINT, endpoints)).execute(); dsl.deleteFrom(dof).where(PostgresqlUtils.in(dof.CONNECTOR_ENDPOINT, endpoints)).execute(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java index ae83ced18..51b5f461e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java @@ -20,7 +20,6 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; -import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -56,7 +55,7 @@ public void addConnectors(DSLContext dsl, Collection connectorEndpoints) private ConnectorRecord newConnectorRow(String endpoint) { var connector = new ConnectorRecord(); connector.setEndpoint(endpoint); - connector.setConnectorId(UrlUtils.getEverythingBeforeThePath(endpoint)); + connector.setParticipantId(""); connector.setCreatedAt(OffsetDateTime.now()); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java index c459269e7..8b46d9c81 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java @@ -12,7 +12,6 @@ * */ - package de.sovity.edc.ext.brokerserver.services; import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index 0a08c44e0..1ba483a06 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -37,8 +37,7 @@ public class CatalogApiService { private final PaginationMetadataUtils paginationMetadataUtils; private final CatalogQueryService catalogQueryService; - private final PolicyDtoBuilder policyDtoBuilder; - private final AssetPropertyParser assetPropertyParser; + private final DataOfferMappingUtils dataOfferMappingUtils; private final CatalogFilterService catalogFilterService; private final BrokerServerSettings brokerServerSettings; @@ -84,11 +83,17 @@ private List buildCatalogDataOffers(List } private CatalogDataOffer buildCatalogDataOffer(DataOfferListEntryRs dataOfferRs) { + var asset = dataOfferMappingUtils.buildUiAsset( + dataOfferRs.getAssetJsonLd(), + dataOfferRs.getConnectorEndpoint(), + dataOfferRs.getConnectorParticipantId() + ); + var dataOffer = new CatalogDataOffer(); dataOffer.setAssetId(dataOfferRs.getAssetId()); dataOffer.setCreatedAt(dataOfferRs.getCreatedAt()); dataOffer.setUpdatedAt(dataOfferRs.getUpdatedAt()); - dataOffer.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOfferRs.getAssetPropertiesJson())); + dataOffer.setAsset(asset); dataOffer.setContractOffers(buildCatalogContractOffers(dataOfferRs)); dataOffer.setConnectorEndpoint(dataOfferRs.getConnectorEndpoint()); dataOffer.setConnectorOfflineSinceOrLastUpdatedAt(dataOfferRs.getConnectorOfflineSinceOrLastUpdatedAt()); @@ -105,7 +110,7 @@ private List buildCatalogContractOffers(DataOfferListEntry private CatalogContractOffer buildCatalogContractOffer(ContractOfferRs contractOfferDbRow) { var contractOffer = new CatalogContractOffer(); contractOffer.setContractOfferId(contractOfferDbRow.getContractOfferId()); - contractOffer.setContractPolicy(policyDtoBuilder.buildPolicyFromJson(contractOfferDbRow.getPolicyJson())); + contractOffer.setContractPolicy(dataOfferMappingUtils.buildUiPolicy(contractOfferDbRow.getPolicyJson())); contractOffer.setCreatedAt(contractOfferDbRow.getCreatedAt()); contractOffer.setUpdatedAt(contractOfferDbRow.getUpdatedAt()); return contractOffer; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index dfaa22851..46e017be9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -14,17 +14,6 @@ package de.sovity.edc.ext.brokerserver.services.api; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorListEntry; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingItem; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorPageQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; @@ -32,109 +21,23 @@ import java.util.List; import java.util.Objects; -import java.util.stream.Stream; import static de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority.ADDED_ON_API_CALL; import static java.util.stream.Collectors.toSet; @RequiredArgsConstructor public class ConnectorApiService { - private final ConnectorPageQueryService connectorPageQueryService; private final ConnectorService connectorService; - private final PaginationMetadataUtils paginationMetadataUtils; private final BrokerEventLogger brokerEventLogger; - public ConnectorPageResult connectorPage(DSLContext dsl, ConnectorPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - - var connectorDbRows = connectorPageQueryService.queryConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); - - var result = new ConnectorPageResult(); - result.setAvailableSortings(buildAvailableSortings()); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); - result.setConnectors(buildConnectorListEntries(connectorDbRows)); - return result; - } - - public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDetailPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - - var connectorDbRow = connectorPageQueryService.queryConnectorDetailPage(dsl, query.getConnectorEndpoint()); - var connector = buildConnectorDetailPageEntry(connectorDbRow); - - var result = new ConnectorDetailPageResult(); - result.setCreatedAt(connector.getCreatedAt()); - result.setEndpoint(connector.getEndpoint()); - result.setId(connector.getId()); - result.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); - result.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); - result.setNumContractOffers(connector.getNumContractOffers()); - result.setOnlineStatus(connector.getOnlineStatus()); - result.setConnectorCrawlingTimeAvg(connector.getConnectorCrawlingTimeAvg()); - return result; - } - - private List buildConnectorListEntries(List connectors) { - return connectors.stream().map(this::buildConnectorListEntry).toList(); - } - - private ConnectorListEntry buildConnectorListEntry(ConnectorListEntryRs connector) { - var dto = new ConnectorListEntry(); - dto.setId(connector.getConnectorId()); - dto.setEndpoint(connector.getEndpoint()); - dto.setCreatedAt(connector.getCreatedAt()); - dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); - dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); - dto.setOnlineStatus(getOnlineStatus(connector)); - dto.setNumContractOffers(connector.getNumDataOffers()); - return dto; - } - - private ConnectorDetailPageResult buildConnectorDetailPageEntry(ConnectorDetailsRs connector) { - var dto = new ConnectorDetailPageResult(); - dto.setId(connector.getConnectorId()); - dto.setEndpoint(connector.getEndpoint()); - dto.setCreatedAt(connector.getCreatedAt()); - dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); - dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); - dto.setOnlineStatus(getOnlineStatus(connector)); - dto.setNumContractOffers(connector.getNumDataOffers()); - dto.setConnectorCrawlingTimeAvg(connector.getConnectorCrawlingTimeAvg()); - return dto; - } - - private ConnectorOnlineStatus getOnlineStatus(ConnectorListEntryRs connector) { - return switch (connector.getOnlineStatus()) { - case ONLINE -> ConnectorOnlineStatus.ONLINE; - case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - case DEAD -> ConnectorOnlineStatus.DEAD; - default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + connector.getOnlineStatus()); - }; - } - - private ConnectorOnlineStatus getOnlineStatus(ConnectorDetailsRs connector) { - return switch (connector.getOnlineStatus()) { - case ONLINE -> ConnectorOnlineStatus.ONLINE; - case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + connector.getOnlineStatus()); - }; - } - - private List buildAvailableSortings() { - return Stream.of( - ConnectorPageSortingType.MOST_RECENT, - ConnectorPageSortingType.TITLE - ).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); - } - public void addConnectors(DSLContext dsl, List connectorEndpoints) { var existingEndpoints = connectorService.getConnectorEndpoints(dsl); var endpoints = connectorEndpoints.stream() - .filter(Objects::nonNull) - .map(String::trim) - .filter(UrlUtils::isValidUrl) - .filter(endpoint -> !existingEndpoints.contains(endpoint)) - .collect(toSet()); + .filter(Objects::nonNull) + .map(String::trim) + .filter(UrlUtils::isValidUrl) + .filter(endpoint -> !existingEndpoints.contains(endpoint)) + .collect(toSet()); connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java new file mode 100644 index 000000000..33ae006e8 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorDetailQueryService; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.Objects; + +@RequiredArgsConstructor +public class ConnectorDetailApiService { + private final ConnectorDetailQueryService connectorDetailQueryService; + private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; + + public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDetailPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); + + var connectorDbRow = connectorDetailQueryService.queryConnectorDetailPage(dsl, query.getConnectorEndpoint()); + var dto = new ConnectorDetailPageResult(); + dto.setParticipantId(connectorDbRow.getParticipantId()); + dto.setEndpoint(connectorDbRow.getEndpoint()); + dto.setCreatedAt(connectorDbRow.getCreatedAt()); + dto.setLastRefreshAttemptAt(connectorDbRow.getLastRefreshAttemptAt()); + dto.setLastSuccessfulRefreshAt(connectorDbRow.getLastSuccessfulRefreshAt()); + dto.setOnlineStatus(connectorOnlineStatusMapper.getOnlineStatus(connectorDbRow.getOnlineStatus())); + dto.setNumDataOffers(connectorDbRow.getNumDataOffers()); + dto.setConnectorCrawlingTimeAvg(connectorDbRow.getConnectorCrawlingTimeAvg()); + return dto; + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java new file mode 100644 index 000000000..fe41c641a --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.ConnectorListEntry; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingItem; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorListQueryService; +import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class ConnectorListApiService { + private final ConnectorListQueryService connectorListQueryService; + private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; + private final PaginationMetadataUtils paginationMetadataUtils; + + public ConnectorPageResult connectorListPage(DSLContext dsl, ConnectorPageQuery query) { + Objects.requireNonNull(query, "query must not be null"); + + var connectorDbRows = connectorListQueryService.queryConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); + + var result = new ConnectorPageResult(); + result.setAvailableSortings(buildAvailableSortings()); + result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); + result.setConnectors(buildConnectorListEntries(connectorDbRows)); + return result; + } + + private List buildConnectorListEntries(List connectors) { + return connectors.stream().map(this::buildConnectorListEntry).toList(); + } + + private ConnectorListEntry buildConnectorListEntry(ConnectorListEntryRs connector) { + var dto = new ConnectorListEntry(); + dto.setParticipantId(connector.getParticipantId()); + dto.setEndpoint(connector.getEndpoint()); + dto.setCreatedAt(connector.getCreatedAt()); + dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); + dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); + dto.setOnlineStatus(connectorOnlineStatusMapper.getOnlineStatus(connector.getOnlineStatus())); + dto.setNumDataOffers(connector.getNumDataOffers()); + return dto; + } + + private List buildAvailableSortings() { + return Stream.of( + ConnectorPageSortingType.MOST_RECENT, + ConnectorPageSortingType.TITLE + ).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java new file mode 100644 index 000000000..15f204711 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ConnectorOnlineStatusMapper { + + public ConnectorOnlineStatus getOnlineStatus(de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus onlineStatus) { + return switch (onlineStatus) { + case ONLINE -> ConnectorOnlineStatus.ONLINE; + case OFFLINE -> ConnectorOnlineStatus.OFFLINE; + case DEAD -> ConnectorOnlineStatus.DEAD; + default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + onlineStatus); + }; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java index f22460146..b3996dcb8 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java @@ -37,7 +37,7 @@ public void addConnectors(DSLContext dsl, Collection connectorEndpoints, public void deleteConnectors(DSLContext dsl, Collection endpoints) { removeConnectorRows(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, endpoints); - removeConnectorRows(dsl, Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, endpoints); + removeConnectorRows(dsl, Tables.CONTRACT_OFFER.CONNECTOR_ENDPOINT, endpoints); removeConnectorRows(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, endpoints); removeConnectorRows(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, endpoints); removeConnectorRows(dsl, Tables.CONNECTOR.ENDPOINT, endpoints); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java index 137743c26..e6a927a7f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java @@ -32,13 +32,17 @@ public class DataOfferDetailApiService { private final DataOfferDetailPageQueryService dataOfferDetailPageQueryService; private final ViewCountLogger viewCountLogger; - private final PolicyDtoBuilder policyDtoBuilder; - private final AssetPropertyParser assetPropertyParser; + private final DataOfferMappingUtils dataOfferMappingUtils; public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDetailPageQuery query) { Objects.requireNonNull(query, "query must not be null"); var dataOffer = dataOfferDetailPageQueryService.queryDataOfferDetailsPage(dsl, query.getAssetId(), query.getConnectorEndpoint()); + var asset = dataOfferMappingUtils.buildUiAsset( + dataOffer.getAssetJsonLd(), + dataOffer.getConnectorEndpoint(), + dataOffer.getConnectorParticipantId() + ); viewCountLogger.increaseDataOfferViewCount(dsl, query.getAssetId(), query.getConnectorEndpoint()); var result = new DataOfferDetailPageResult(); @@ -46,7 +50,7 @@ public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDe result.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); result.setConnectorOnlineStatus(mapConnectorOnlineStatus(dataOffer.getConnectorOnlineStatus())); result.setConnectorOfflineSinceOrLastUpdatedAt(dataOffer.getConnectorOfflineSinceOrLastUpdatedAt()); - result.setProperties(assetPropertyParser.parsePropertiesFromJsonString(dataOffer.getAssetPropertiesJson())); + result.setAsset(asset); result.setCreatedAt(dataOffer.getCreatedAt()); result.setUpdatedAt(dataOffer.getUpdatedAt()); result.setContractOffers(buildDataOfferDetailContractOffers(dataOffer.getContractOffers())); @@ -78,7 +82,7 @@ private DataOfferDetailContractOffer buildDataOfferDetailContractOffer(ContractO newOffer.setCreatedAt(offer.getCreatedAt()); newOffer.setUpdatedAt(offer.getUpdatedAt()); newOffer.setContractOfferId(offer.getContractOfferId()); - newOffer.setContractPolicy(policyDtoBuilder.buildPolicyFromJson(offer.getPolicyJson())); + newOffer.setContractPolicy(dataOfferMappingUtils.buildUiPolicy(offer.getPolicyJson())); return newOffer; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java new file mode 100644 index 000000000..e683bf298 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import de.sovity.edc.utils.JsonUtils; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class DataOfferMappingUtils { + private final PolicyMapper policyMapper; + private final AssetMapper assetMapper; + + public UiAsset buildUiAsset(String assetJsonLd, String endpoint, String participantId) { + var asset = assetMapper.buildAsset(JsonUtils.parseJsonObj(assetJsonLd)); + return assetMapper.buildUiAsset(asset, endpoint, participantId); + } + + public UiPolicy buildUiPolicy(String policyJson) { + var policy = policyMapper.buildPolicy(policyJson); + return policyMapper.buildUiPolicy(policy); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java deleted file mode 100644 index 5768ee970..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PolicyDtoBuilder.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.wrapper.api.common.model.ExpressionDto; -import de.sovity.edc.ext.wrapper.api.common.model.PermissionDto; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDto; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -@RequiredArgsConstructor -public class PolicyDtoBuilder { - - @SneakyThrows - public PolicyDto buildPolicyFromJson(@NonNull String policyJson) { - var policyDto = new PolicyDto(); - policyDto.setLegacyPolicy(policyJson); - policyDto.setPermission(extractPermissions(policyJson)); - return policyDto; - } - - @NotNull - private PermissionDto extractPermissions(String policyJson) { - // TODO - return new PermissionDto(new ExpressionDto(ExpressionDto.Type.AND, null, List.of(), null, null)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java index a54ec8f8e..088b2cc84 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java @@ -16,27 +16,31 @@ import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import org.jetbrains.annotations.NotNull; import org.jooq.Field; -import org.jooq.impl.DSL; + +import java.util.function.Function; public class CatalogFilterAttributeDefinitionService { - public CatalogFilterAttributeDefinition fromAssetProperty(String assetProperty, String label) { + public CatalogFilterAttributeDefinition forField( + Function> fieldExtractor, + String name, + String label + ) { return new CatalogFilterAttributeDefinition( - assetProperty, - label, - fields -> getValue(fields, assetProperty), - (fields, values) -> PostgresqlUtils.in(getValue(fields, assetProperty), values) + name, + label, + fieldExtractor::apply, + (fields, values) -> PostgresqlUtils.in(fieldExtractor.apply(fields), values) ); } public CatalogFilterAttributeDefinition buildDataSpaceFilter() { return new CatalogFilterAttributeDefinition( - "dataSpace", - "Data Space", - CatalogQueryFields::getDataSpace, - (fields, values) -> PostgresqlUtils.in(fields.getDataSpace(), values) + "dataSpace", + "Data Space", + CatalogQueryFields::getDataSpace, + (fields, values) -> PostgresqlUtils.in(fields.getDataSpace(), values) ); } @@ -48,9 +52,4 @@ public CatalogFilterAttributeDefinition buildConnectorEndpointFilter() { (fields, values) -> PostgresqlUtils.in(fields.getDataOfferTable().CONNECTOR_ENDPOINT, values) ); } - - @NotNull - private Field getValue(CatalogQueryFields fields, String assetProperty) { - return DSL.coalesce(fields.getAssetProperty(assetProperty), DSL.value("")); - } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index 73a6cce17..094c2cdfa 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -19,11 +19,10 @@ import de.sovity.edc.ext.brokerserver.api.model.CnfFilterItem; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValue; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValueAttribute; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; @@ -58,40 +57,50 @@ public class CatalogFilterService { */ private List getAvailableFilters() { return List.of( - catalogFilterAttributeDefinitionService.buildDataSpaceFilter(), - catalogFilterAttributeDefinitionService.fromAssetProperty( - AssetProperty.DATA_CATEGORY, - "Data Category" - ), - catalogFilterAttributeDefinitionService.fromAssetProperty( - AssetProperty.DATA_SUBCATEGORY, - "Data Subcategory" - ), - catalogFilterAttributeDefinitionService.fromAssetProperty( - AssetProperty.DATA_MODEL, - "Data Model" - ), - catalogFilterAttributeDefinitionService.fromAssetProperty( - AssetProperty.TRANSPORT_MODE, - "Transport Mode" - ), - catalogFilterAttributeDefinitionService.fromAssetProperty( - AssetProperty.GEO_REFERENCE_METHOD, - "Geo Reference Method" - ), - catalogFilterAttributeDefinitionService.buildConnectorEndpointFilter() + catalogFilterAttributeDefinitionService.buildDataSpaceFilter(), + catalogFilterAttributeDefinitionService.forField( + fields -> fields.getDataOfferTable().DATA_CATEGORY, + "dataCategory", + "Data Category" + ), + catalogFilterAttributeDefinitionService.forField( + fields -> fields.getDataOfferTable().DATA_SUBCATEGORY, + "dataSubcategory", + "Data Subcategory" + ), + catalogFilterAttributeDefinitionService.forField( + fields -> fields.getDataOfferTable().DATA_MODEL, + "dataModel", + "Data Model" + ), + catalogFilterAttributeDefinitionService.forField( + fields -> fields.getDataOfferTable().TRANSPORT_MODE, + "transportMode", + "Transport Mode" + ), + catalogFilterAttributeDefinitionService.forField( + fields -> fields.getDataOfferTable().GEO_REFERENCE_METHOD, + "geoReferenceMethod", + "Geo Reference Method" + ), + catalogFilterAttributeDefinitionService.forField( + fields -> fields.getDataOfferTable().CURATOR_ORGANIZATION_NAME, + "curatorOrganizationName", + "Organization Name" + ), + catalogFilterAttributeDefinitionService.buildConnectorEndpointFilter() ); } public List getCatalogQueryFilters(CnfFilterValue cnfFilterValue) { var values = getCnfFilterValuesMap(cnfFilterValue); return getAvailableFilters().stream() - .map(filter -> new CatalogQueryFilter( - filter.name(), - filter.valueGetter(), - getQueryFilter(filter, values.get(filter.name())) - )) - .toList(); + .map(filter -> new CatalogQueryFilter( + filter.name(), + filter.valueGetter(), + getQueryFilter(filter, values.get(filter.name())) + )) + .toList(); } private CatalogQuerySelectedFilterQuery getQueryFilter(CatalogFilterAttributeDefinition filter, List values) { @@ -102,34 +111,34 @@ private CatalogQuerySelectedFilterQuery getQueryFilter(CatalogFilterAttributeDef } public CnfFilter buildAvailableFilters(String filterValuesJson) { - var filterValues = JsonDeserializationUtils.deserializeStringArray2(filterValuesJson); + var filterValues = JsonDeserializationUtils.read2dStringList(filterValuesJson); var filterAttributes = zipAvailableFilters(getAvailableFilters(), filterValues) - .map(availableFilter -> new CnfFilterAttribute( - availableFilter.definition().name(), - availableFilter.definition().label(), - buildAvailableFilterValues(availableFilter) - )) - .toList(); + .map(availableFilter -> new CnfFilterAttribute( + availableFilter.definition().name(), + availableFilter.definition().label(), + buildAvailableFilterValues(availableFilter) + )) + .toList(); return new CnfFilter(filterAttributes); } private List buildAvailableFilterValues(AvailableFilter availableFilter) { return availableFilter.availableValues().stream() - .sorted(caseInsensitiveEmptyStringLast) - .map(value -> new CnfFilterItem(value, value)) - .toList(); + .sorted(caseInsensitiveEmptyStringLast) + .map(value -> new CnfFilterItem(value, value)) + .toList(); } private Stream zipAvailableFilters(List availableFilters, List> filterValues) { Validate.isTrue( - availableFilters.size() == filterValues.size(), - "Number of available filters and filter values must match: %d != %d", - availableFilters.size(), - filterValues.size() + availableFilters.size() == filterValues.size(), + "Number of available filters and filter values must match: %d != %d", + availableFilters.size(), + filterValues.size() ); return Stream.iterate(0, i -> i + 1) - .limit(availableFilters.size()) - .map(i -> new AvailableFilter(availableFilters.get(i), filterValues.get(i))); + .limit(availableFilters.size()) + .map(i -> new AvailableFilter(availableFilters.get(i), filterValues.get(i))); } private record AvailableFilter(CatalogFilterAttributeDefinition definition, List availableValues) { @@ -140,7 +149,7 @@ private Map> getCnfFilterValuesMap(CnfFilterValue cnfFilter return Map.of(); } return cnfFilterValue.getSelectedAttributeValues().stream() - .filter(it -> it.getId() != null && CollectionUtils2.isNotEmpty(it.getSelectedIds())) - .collect(toMap(CnfFilterValueAttribute::getId, CnfFilterValueAttribute::getSelectedIds)); + .filter(it -> it.getId() != null && CollectionUtils2.isNotEmpty(it.getSelectedIds())) + .collect(toMap(CnfFilterValueAttribute::getId, CnfFilterValueAttribute::getSelectedIds)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java new file mode 100644 index 000000000..0c4c89bfa --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api.filtering; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; +import lombok.RequiredArgsConstructor; +import org.jooq.Condition; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogSearchService { + + public Condition filterBySearch(CatalogQueryFields fields, String searchQuery) { + return SearchUtils.simpleSearch(searchQuery, List.of( + fields.getDataOfferTable().ASSET_ID, + fields.getDataOfferTable().ASSET_TITLE, + fields.getDataOfferTable().DATA_CATEGORY, + fields.getDataOfferTable().DATA_SUBCATEGORY, + fields.getDataOfferTable().DESCRIPTION, + fields.getDataOfferTable().CURATOR_ORGANIZATION_NAME, + fields.getDataOfferTable().KEYWORDS_COMMA_JOINED, + fields.getConnectorTable().ENDPOINT + )); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java index 5e8d57cf3..589f08aa0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java @@ -12,7 +12,6 @@ * */ - package de.sovity.edc.ext.brokerserver.services.config; /** diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java index b34688f7c..1513c1f5e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java @@ -34,8 +34,11 @@ public class ConnectorChangeTracker { @Setter private int numOffersUpdated = 0; + @Setter + private String participantIdChanged = null; + public boolean isEmpty() { - return numOffersAdded == 0 && numOffersDeleted == 0 && numOffersUpdated == 0; + return numOffersAdded == 0 && numOffersDeleted == 0 && numOffersUpdated == 0 && participantIdChanged == null; } @Override @@ -58,6 +61,9 @@ public String toString() { } msg += " Data Offers changed: %s.".formatted(String.join(", ", offersMsgs)); } + if (participantIdChanged != null) { + msg += " Participant ID changed to %s.".formatted(participantIdChanged); + } return msg; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index dc2c3b6a9..8b4279b64 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -20,12 +20,12 @@ import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; import java.time.OffsetDateTime; -import java.util.Collection; +import java.util.Objects; @RequiredArgsConstructor public class ConnectorUpdateSuccessWriter { @@ -36,12 +36,10 @@ public class ConnectorUpdateSuccessWriter { public void handleConnectorOnline( DSLContext dsl, ConnectorRecord connector, - Collection dataOffers + FetchedCatalog catalog ) { - var now = OffsetDateTime.now(); - // Limit data offers and log limitation if necessary - var limitedDataOffers = dataOfferLimitsEnforcer.enforceLimits(dataOffers); + var limitedDataOffers = dataOfferLimitsEnforcer.enforceLimits(catalog.getDataOffers()); dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, limitedDataOffers); // Log Status Change and set status to online if necessary @@ -52,16 +50,27 @@ public void handleConnectorOnline( // Track changes for final log message var changes = new ConnectorChangeTracker(); - connector.setLastSuccessfulRefreshAt(now); - connector.setLastRefreshAttemptAt(now); - connector.update(); + updateConnector(connector, catalog, changes); - // Log Event if changes are present + // Update data offers + dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), limitedDataOffers.abbreviatedDataOffers(), changes); + + // Log event if changes are present if (!changes.isEmpty()) { brokerEventLogger.logConnectorUpdated(dsl, connector.getEndpoint(), changes); } + } - // Update data offers - dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), limitedDataOffers.abbreviatedDataOffers(), changes); + private static void updateConnector(ConnectorRecord connector, FetchedCatalog catalog, ConnectorChangeTracker changes) { + var now = OffsetDateTime.now(); + var participantId = catalog.getParticipantId(); + + connector.setLastSuccessfulRefreshAt(now); + connector.setLastRefreshAttemptAt(now); + if (!Objects.equals(connector.getParticipantId(), participantId)) { + connector.setParticipantId(participantId); + changes.setParticipantIdChanged(participantId); + } + connector.update(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java index 591c45f34..5a759fab0 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java @@ -19,7 +19,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferFetcher; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.CatalogFetcher; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.time.StopWatch; import org.eclipse.edc.spi.monitor.Monitor; @@ -31,7 +31,7 @@ */ @RequiredArgsConstructor public class ConnectorUpdater { - private final DataOfferFetcher dataOfferFetcher; + private final CatalogFetcher catalogFetcher; private final ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter; private final ConnectorUpdateFailureWriter connectorUpdateFailureWriter; private final ConnectorQueries connectorQueries; @@ -51,12 +51,12 @@ public void updateConnector(String connectorEndpoint) { try { monitor.info("Updating connector: " + connectorEndpoint); - var dataOffers = dataOfferFetcher.fetch(connectorEndpoint); + var catalog = catalogFetcher.fetchCatalog(connectorEndpoint); // Update connector in a single transaction dslContextFactory.transaction(dsl -> { ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); - connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, dataOffers); + connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, catalog); }); } catch (Exception e) { failed = true; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java similarity index 63% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java index d87705716..2b7da851c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java @@ -14,17 +14,17 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.utils.catalog.DspCatalogService; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; -import java.util.Collection; - @RequiredArgsConstructor -public class DataOfferFetcher { - private final ContractOfferFetcher contractOfferFetcher; - private final DataOfferBuilder dataOfferBuilder; +public class CatalogFetcher { + private final DspCatalogService dspCatalogService; + private final FetchedCatalogBuilder fetchedCatalogBuilder; /** * Fetches {@link ContractOffer}s and de-duplicates them into {@link FetchedDataOffer}s. @@ -33,11 +33,8 @@ public class DataOfferFetcher { * @return updated connector db row */ @SneakyThrows - public Collection fetch(String connectorEndpoint) { - // Contract Offers contain assets multiple times, with different policies - var contractOffers = contractOfferFetcher.fetch(connectorEndpoint); - - // Data Offers represent unique assets - return dataOfferBuilder.deduplicateContractOffers(contractOffers); + public FetchedCatalog fetchCatalog(String connectorEndpoint) { + var dspCatalog = dspCatalogService.fetchDataOffers(connectorEndpoint); + return fetchedCatalogBuilder.buildFetchedCatalog(dspCatalog); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java deleted file mode 100644 index fcdd370e1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferFetcher.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.services.refreshing.exceptions.ConnectorUnreachableException; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; -import org.eclipse.edc.connector.spi.catalog.CatalogService; -import org.eclipse.edc.spi.query.QuerySpec; - -import java.util.List; - -@RequiredArgsConstructor -public class ContractOfferFetcher { - private final CatalogService catalogService; - - /** - * Fetches Connector contract offers - * - * @param connectorEndpoint connector endpoint - * @return updated connector db row - */ - @SneakyThrows - public List fetch(String connectorEndpoint) { - try { - return catalogService.getByProviderUrl(connectorEndpoint, QuerySpec.max()).get().getContractOffers(); - } catch (InterruptedException e) { - throw e; - } catch (Exception e) { - throw new ConnectorUnreachableException("Failed to fetch connector contract offers", e); - } - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java index 8e9854f79..3b3980294 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java @@ -15,9 +15,9 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; import de.sovity.edc.ext.brokerserver.dao.utils.JsonbUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; import lombok.RequiredArgsConstructor; import org.jooq.JSONB; @@ -25,7 +25,7 @@ import java.util.Objects; /** - * Creates or updates {@link DataOfferContractOfferRecord} DB Rows. + * Creates or updates {@link ContractOfferRecord} DB Rows. *

* (Or at least prepares them for batch inserts / updates) */ @@ -33,14 +33,14 @@ public class ContractOfferRecordUpdater { /** - * Create new {@link DataOfferContractOfferRecord} from {@link FetchedDataOfferContractOffer}. + * Create new {@link ContractOfferRecord} from {@link FetchedContractOffer}. * * @param dataOffer parent data offer db row * @param fetchedContractOffer fetched contract offer * @return new db row */ - public DataOfferContractOfferRecord newContractOffer(DataOfferRecord dataOffer, FetchedDataOfferContractOffer fetchedContractOffer) { - var contractOffer = new DataOfferContractOfferRecord(); + public ContractOfferRecord newContractOffer(DataOfferRecord dataOffer, FetchedContractOffer fetchedContractOffer) { + var contractOffer = new ContractOfferRecord(); contractOffer.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); contractOffer.setContractOfferId(fetchedContractOffer.getContractOfferId()); contractOffer.setAssetId(dataOffer.getAssetId()); @@ -50,13 +50,13 @@ public DataOfferContractOfferRecord newContractOffer(DataOfferRecord dataOffer, } /** - * Update existing {@link DataOfferContractOfferRecord} with changes from {@link FetchedDataOfferContractOffer}. + * Update existing {@link ContractOfferRecord} with changes from {@link FetchedContractOffer}. * * @param contractOffer existing row * @param fetchedContractOffer changes to be integrated * @return if anything was changed */ - public boolean updateContractOffer(DataOfferContractOfferRecord contractOffer, FetchedDataOfferContractOffer fetchedContractOffer) { + public boolean updateContractOffer(ContractOfferRecord contractOffer, FetchedContractOffer fetchedContractOffer) { var existingPolicy = JsonbUtils.getDataOrNull(contractOffer.getPolicy()); var fetchedPolicy = fetchedContractOffer.getPolicyJson(); var changed = false; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java deleted file mode 100644 index 25c4f5e83..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferBuilder.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; -import de.sovity.edc.ext.brokerserver.utils.StreamUtils2; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.apache.commons.lang3.tuple.Pair; -import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; -import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.jetbrains.annotations.NotNull; - -import java.net.URI; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.groupingBy; - -@RequiredArgsConstructor -public class DataOfferBuilder { - private final ObjectMapper objectMapper; - - /** - * De-duplicates {@link ContractOffer}s into {@link FetchedDataOffer}s. - *

- * Also de-duplicates {@link ContractOffer}s into {@link FetchedDataOfferContractOffer}s. - * - * @param contractOffers {@link ContractOffer}s - * @return {@link FetchedDataOffer}s - */ - public Collection deduplicateContractOffers(Collection contractOffers) { - return groupByAssetId(contractOffers) - .stream() - .map(offers -> buildFetchedDataOffer(offers.get(0).getAsset(), offers)) - .toList(); - } - - @NotNull - private FetchedDataOffer buildFetchedDataOffer(Asset asset, List offers) { - var dataOffer = new FetchedDataOffer(); - dataOffer.setAssetId(asset.getId()); - dataOffer.setAssetName(getAssetName(asset)); - dataOffer.setAssetPropertiesJson(getAssetPropertiesJson(asset)); - dataOffer.setContractOffers(buildFetchedDataOfferContractOffers(offers)); - return dataOffer; - } - - @NotNull - private List buildFetchedDataOfferContractOffers(List offers) { - return offers.stream() - .map(this::buildFetchedDataOfferContractOffer) - .filter(StreamUtils2.distinctByKey(FetchedDataOfferContractOffer::getContractOfferId)) - .toList(); - } - - @NotNull - private FetchedDataOfferContractOffer buildFetchedDataOfferContractOffer(ContractOffer offer) { - var contractOffer = new FetchedDataOfferContractOffer(); - contractOffer.setContractOfferId(offer.getId()); - contractOffer.setPolicyJson(getPolicyJson(offer)); - return contractOffer; - } - - private Collection> groupByAssetId(Collection contractOffers) { - return contractOffers.stream().collect(groupingBy(offer -> offer.getAsset().getId())).values(); - } - - private String getAssetName(Asset asset) { - String assetName = asset.getName(); - if (assetName == null) { - assetName = asset.getId(); - } - return assetName; - } - - @NotNull - @SneakyThrows - private String getAssetPropertiesJson(Asset asset) { - var properties = mapNonNullValues(asset.getProperties(), this::getAssetPropertyValue); - return objectMapper.writeValueAsString(properties); - } - - @SneakyThrows - private String getAssetPropertyValue(Object value) { - if (value == null) { - return null; - } - - if (value instanceof URI uri) { - // I don't know why the Eclipse EDC is casting Strings to URIs, but it does - // We need to prevent this from hitting the writeValueAsString or additional - // quotes are added - return uri.toString(); - } - - if (value instanceof String stringValue) { - return stringValue; - } - - // Using JSON Properties in the MS8 EDC causes the broker to fail - // this is why we map them to their JSON to "show them", but not fail due to them - return objectMapper.writeValueAsString(value); - } - - @NotNull - @SneakyThrows - private String getPolicyJson(ContractOffer offer) { - return objectMapper.writeValueAsString(offer.getPolicy()); - } - - private Map mapNonNullValues(Map map, Function valueMapper) { - return map.entrySet().stream() - .map(entry -> Pair.of(entry.getKey(), valueMapper.apply(entry.getValue()))) - .filter(entry -> entry.getValue() != null) - .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java index 959885022..8b7dcc6be 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java @@ -14,13 +14,13 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.ContractOfferQueries; import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.DataOfferPatch; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -32,7 +32,7 @@ @RequiredArgsConstructor public class DataOfferPatchBuilder { - private final DataOfferContractOfferQueries dataOfferContractOfferQueries; + private final ContractOfferQueries contractOfferQueries; private final DataOfferQueries dataOfferQueries; private final DataOfferRecordUpdater dataOfferRecordUpdater; private final ContractOfferRecordUpdater contractOfferRecordUpdater; @@ -52,9 +52,9 @@ public DataOfferPatch buildDataOfferPatch( ) { var patch = new DataOfferPatch(); var dataOffers = dataOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint); - var contractOffersByAssetId = dataOfferContractOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint) + var contractOffersByAssetId = contractOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint) .stream() - .collect(groupingBy(DataOfferContractOfferRecord::getAssetId)); + .collect(groupingBy(ContractOfferRecord::getAssetId)); var diff = DiffUtils.compareLists( dataOffers, @@ -97,16 +97,16 @@ public DataOfferPatch buildDataOfferPatch( private boolean patchContractOffers( DataOfferPatch patch, DataOfferRecord dataOffer, - Collection contractOffers, - Collection fetchedContractOffers + Collection contractOffers, + Collection fetchedContractOffers ) { var hasUpdates = new AtomicBoolean(false); var diff = DiffUtils.compareLists( contractOffers, - DataOfferContractOfferRecord::getContractOfferId, + ContractOfferRecord::getContractOfferId, fetchedContractOffers, - FetchedDataOfferContractOffer::getContractOfferId + FetchedContractOffer::getContractOfferId ); diff.added().forEach(fetched -> { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java index 2676ffb46..97b1e2102 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java @@ -17,11 +17,17 @@ import de.sovity.edc.ext.brokerserver.dao.utils.JsonbUtils; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.brokerserver.utils.JsonUtils2; import lombok.RequiredArgsConstructor; import org.jooq.JSONB; import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; /** * Creates or updates {@link DataOfferRecord} DB Rows. @@ -55,20 +61,78 @@ public DataOfferRecord newDataOffer(String connectorEndpoint, FetchedDataOffer f * @param changed whether the data offer should be marked as updated simply because the contract offers changed * @return whether any fields were updated */ - public boolean updateDataOffer(DataOfferRecord dataOffer, FetchedDataOffer fetchedDataOffer, boolean changed) { - if (!Objects.equals(fetchedDataOffer.getAssetName(), dataOffer.getAssetName())) { - Objects.requireNonNull(fetchedDataOffer.getAssetName(), - "Fetched data offer's asset name should have been set as id if name isn't present"); - dataOffer.setAssetName(fetchedDataOffer.getAssetName()); - changed = true; - } + public boolean updateDataOffer( + DataOfferRecord dataOffer, + FetchedDataOffer fetchedDataOffer, + boolean changed + ) { + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getAssetTitle, + DataOfferRecord::getAssetTitle, + dataOffer::setAssetTitle + ); - String existingAssetProps = JsonbUtils.getDataOrNull(dataOffer.getAssetProperties()); - var fetchedAssetProps = fetchedDataOffer.getAssetPropertiesJson(); - if (!Objects.equals(fetchedAssetProps, existingAssetProps)) { - dataOffer.setAssetProperties(JSONB.jsonb(fetchedAssetProps)); - changed = true; - } + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getDescription, + DataOfferRecord::getDescription, + dataOffer::setDescription + ); + + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getCuratorOrganizationName, + DataOfferRecord::getCuratorOrganizationName, + dataOffer::setCuratorOrganizationName + ); + + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getDataCategory, + DataOfferRecord::getDataCategory, + dataOffer::setDataCategory + ); + + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getDataSubcategory, + DataOfferRecord::getDataSubcategory, + dataOffer::setDataSubcategory + ); + + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getDataModel, + DataOfferRecord::getDataModel, + dataOffer::setDataModel + ); + + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getTransportMode, + DataOfferRecord::getTransportMode, + dataOffer::setTransportMode + ); + + changed |= updateField( + dataOffer, + fetchedDataOffer, + FetchedDataOffer::getGeoReferenceMethod, + DataOfferRecord::getGeoReferenceMethod, + dataOffer::setGeoReferenceMethod + ); + + changed |= updateKeywords(dataOffer, fetchedDataOffer); + + changed |= updateAssetJsonLd(dataOffer, fetchedDataOffer); if (changed) { dataOffer.setUpdatedAt(OffsetDateTime.now()); @@ -76,4 +140,67 @@ public boolean updateDataOffer(DataOfferRecord dataOffer, FetchedDataOffer fetch return changed; } + + private boolean updateField( + DataOfferRecord dataOffer, + FetchedDataOffer fetchedDataOffer, + Function fetchedField, + Function existingField, + Consumer setter + ) { + var fetched = fetchedField.apply(fetchedDataOffer); + if (fetched == null) { + fetched = ""; + } + + var existing = existingField.apply(dataOffer); + if (existing == null) { + existing = ""; + } + + + if (Objects.equals(fetched, existing)) { + return false; + } + + setter.accept(fetched); + return true; + } + + private boolean updateKeywords( + DataOfferRecord dataOffer, + FetchedDataOffer fetchedDataOffer + ) { + List fetched = fetchedDataOffer.getKeywords(); + if (fetched == null) { + fetched = List.of(); + } + + String[] existing = dataOffer.getKeywords(); + if (existing == null) { + existing = new String[0]; + } + + if (Objects.equals(new HashSet<>(fetched), new HashSet<>(Arrays.asList(existing)))) { + return false; + } + + dataOffer.setKeywords(fetched.toArray(new String[0])); + dataOffer.setKeywordsCommaJoined(String.join(",", fetched)); + return true; + } + + private boolean updateAssetJsonLd( + DataOfferRecord dataOffer, + FetchedDataOffer fetchedDataOffer + ) { + String existing = JsonbUtils.getDataOrNull(dataOffer.getAssetJsonLd()); + var fetched = fetchedDataOffer.getAssetJsonLd(); + if (JsonUtils2.isEqualJson(fetched, existing)) { + return false; + } + + dataOffer.setAssetJsonLd(JSONB.jsonb(fetched)); + return true; + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java new file mode 100644 index 000000000..64d850281 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers; + +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.catalog.model.DspCatalog; +import de.sovity.edc.utils.catalog.model.DspContractOffer; +import de.sovity.edc.utils.catalog.model.DspDataOffer; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@RequiredArgsConstructor +public class FetchedCatalogBuilder { + private final AssetMapper assetMapper; + + public FetchedCatalog buildFetchedCatalog(DspCatalog catalog) { + var participantId = catalog.getParticipantId(); + + var fetchedDataOffers = catalog.getDataOffers().stream() + .map(dspDataOffer -> buildFetchedDataOffer(dspDataOffer, participantId)) + .toList(); + + var fetchedCatalog = new FetchedCatalog(); + fetchedCatalog.setParticipantId(participantId); + fetchedCatalog.setDataOffers(fetchedDataOffers); + + return fetchedCatalog; + } + + @NotNull + private FetchedDataOffer buildFetchedDataOffer(DspDataOffer dspDataOffer, String participantId) { + var assetJsonLd = assetMapper.buildAssetJsonLdFromDatasetProperties(dspDataOffer.getAssetPropertiesJsonLd()); + + var fetchedDataOffer = new FetchedDataOffer(); + setAssetMetadata(fetchedDataOffer, assetJsonLd, participantId); + fetchedDataOffer.setContractOffers(buildFetchedContractOffers(dspDataOffer.getContractOffers())); + return fetchedDataOffer; + } + + @NotNull + private List buildFetchedContractOffers(List offers) { + return offers.stream() + .map(this::buildFetchedContractOffer) + .toList(); + } + + @NotNull + private FetchedContractOffer buildFetchedContractOffer(DspContractOffer offer) { + var contractOffer = new FetchedContractOffer(); + contractOffer.setContractOfferId(offer.getContractOfferId()); + contractOffer.setPolicyJson(JsonUtils.toJson(offer.getPolicyJsonLd())); + return contractOffer; + } + + /** + * This method was extract so tests could re-use the logic of assetJsonLd -> fetchedDataOffer -> dataOfferRecord + * + * @param fetchedDataOffer fetchedDataOffer + * @param assetJsonLd assetJsonLd + */ + public void setAssetMetadata(FetchedDataOffer fetchedDataOffer, JsonObject assetJsonLd, String participantId) { + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, "http://if-you-see-this-this-is-a-bug", participantId); + fetchedDataOffer.setAssetId(uiAsset.getAssetId()); + fetchedDataOffer.setAssetJsonLd(JsonUtils.toJson(assetJsonLd)); + + // Most of these fields are extracted so our DB does not need to + // semantically interpret JSON-LD when sorting, searching and filtering + fetchedDataOffer.setAssetTitle(uiAsset.getTitle()); + fetchedDataOffer.setDescription(uiAsset.getDescription()); + fetchedDataOffer.setCuratorOrganizationName(uiAsset.getCreatorOrganizationName()); + + fetchedDataOffer.setDataCategory(uiAsset.getDataCategory()); + fetchedDataOffer.setDataSubcategory(uiAsset.getDataSubcategory()); + fetchedDataOffer.setDataModel(uiAsset.getDataModel()); + fetchedDataOffer.setGeoReferenceMethod(uiAsset.getGeoReferenceMethod()); + fetchedDataOffer.setTransportMode(uiAsset.getTransportMode()); + fetchedDataOffer.setKeywords(uiAsset.getKeywords()); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java index e9bf5ad69..a7a2b49bd 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import lombok.AccessLevel; import lombok.Getter; @@ -35,9 +35,9 @@ public class DataOfferPatch { List dataOffersToUpdate = new ArrayList<>(); List dataOffersToDelete = new ArrayList<>(); - List contractOffersToInsert = new ArrayList<>(); - List contractOffersToUpdate = new ArrayList<>(); - List contractOffersToDelete = new ArrayList<>(); + List contractOffersToInsert = new ArrayList<>(); + List contractOffersToUpdate = new ArrayList<>(); + List contractOffersToDelete = new ArrayList<>(); public void insertDataOffer(DataOfferRecord offer) { dataOffersToInsert.add(offer); @@ -51,15 +51,15 @@ public void deleteDataOffer(DataOfferRecord offer) { dataOffersToDelete.add(offer); } - public void insertContractOffer(DataOfferContractOfferRecord offer) { + public void insertContractOffer(ContractOfferRecord offer) { contractOffersToInsert.add(offer); } - public void updateContractOffer(DataOfferContractOfferRecord offer) { + public void updateContractOffer(ContractOfferRecord offer) { contractOffersToUpdate.add(offer); } - public void deleteContractOffer(DataOfferContractOfferRecord offer) { + public void deleteContractOffer(ContractOfferRecord offer) { contractOffersToDelete.add(offer); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java new file mode 100644 index 000000000..550dd0657 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +/** + * Contains catalog response as required for writing into DB. + */ +@Getter +@Setter +@FieldDefaults(level = AccessLevel.PRIVATE) +public class FetchedCatalog { + String participantId; + List dataOffers; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedContractOffer.java similarity index 93% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java rename to extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedContractOffer.java index 76c099dfa..b2d566f70 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOfferContractOffer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedContractOffer.java @@ -22,7 +22,7 @@ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) -public class FetchedDataOfferContractOffer { +public class FetchedContractOffer { String contractOfferId; String policyJson; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java index 78cf0c9a1..d93306613 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java @@ -21,12 +21,23 @@ import java.util.List; +/** + * Contains data offer response as required for writing into DB. + */ @Getter @Setter @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedDataOffer { String assetId; - String assetName; - String assetPropertiesJson; - List contractOffers; + String assetTitle; + String description; + String curatorOrganizationName; + String dataCategory; + String dataSubcategory; + String dataModel; + String transportMode; + String geoReferenceMethod; + List keywords; + String assetJsonLd; + List contractOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java new file mode 100644 index 000000000..fbe1af366 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonUtils2 { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @SneakyThrows + public static boolean isEqualJson(String json, String otherJson) { + return + (json == null && otherJson == null) || + (json != null && otherJson != null && OBJECT_MAPPER.readTree(json).equals(OBJECT_MAPPER.readTree(otherJson))); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java index f728d05ef..ae720063b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java @@ -18,35 +18,12 @@ import lombok.NoArgsConstructor; import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class UrlUtils { - - /** - * Returns everything before the URLs path. - *

- * Example: http://www.example.com/path/to/my/file.html -> http://www.example.com - * Example 2: http://www.example.com:9000/path/to/my/file.html -> http://www.example.com:9000 - * - * @param url url - * @return protocol, host, port - */ - public static String getEverythingBeforeThePath(String url) { - var uri = URI.create(url); - var scheme = uri.getScheme(); // "http" - var authority = uri.getAuthority(); // "www.example.com" - int port = uri.getPort(); // -1 (no port specified) - var everythingBeforePath = scheme + "://" + authority; - if (port != -1) { - everythingBeforePath += ":" + port; - } - return everythingBeforePath; - } - public static boolean isValidUrl(String url) { try { new URL(url).toURI(); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java index 781aa4504..c810d36eb 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver; +import de.sovity.edc.ext.brokerserver.client.gen.JSON; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.SneakyThrows; @@ -26,4 +27,8 @@ public class AssertionUtils { public static void assertEqualJson(String expected, String actual) { JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT); } + + public static void assertEqualUsingJson(Object expected, Object actual) { + assertEqualJson(JSON.serialize(expected), JSON.serialize(actual)); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java new file mode 100644 index 000000000..c954d6a86 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TestAsset { + public static JsonObject getAssetJsonLd(String assetId) { + return getAssetJsonLd(assetId, Map.of()); + } + + public static JsonObject getAssetJsonLd(String assetId, Map properties) { + return Json.createObjectBuilder() + .add(Prop.ID, assetId) + .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder() + .add(Prop.Edc.ASSET_ID, assetId) + .addAll(Json.createObjectBuilder(properties))) + .build(); + } + + /** + * Sets assetJsonLd and other extracted fields. + *

+ * This method keeps our tests consistent if we change the extracted fields. + * + * @param dataOfferRecord data offer record to be updated + * @param assetJsonLd asset json ld + * @param participantId required because the organization name will default to the participant id if unset + */ + public static void setDataOfferAssetMetadata(DataOfferRecord dataOfferRecord, JsonObject assetJsonLd, String participantId) { + // We trickily use the real code to update all the extracted values from the asset JSON-LD + var fetchedCatalogBuilder = BrokerServerExtensionContext.instance.fetchedCatalogBuilder(); + var dataOfferRecordUpdater = BrokerServerExtensionContext.instance.dataOfferRecordUpdater(); + + var fetchedDataOffer = new FetchedDataOffer(); + fetchedCatalogBuilder.setAssetMetadata(fetchedDataOffer, assetJsonLd, participantId); + + dataOfferRecord.setAssetId(fetchedDataOffer.getAssetId()); + dataOfferRecordUpdater.updateDataOffer(dataOfferRecord, fetchedDataOffer, false); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java new file mode 100644 index 000000000..3087968d9 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver; + +import de.sovity.edc.ext.brokerserver.client.gen.JSON; +import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteralType; +import de.sovity.edc.utils.JsonUtils; +import org.jooq.JSONB; + +import java.time.OffsetDateTime; +import java.util.List; + +public class TestPolicy { + private static OffsetDateTime today = OffsetDateTime.now(); + + public static UiPolicyConstraint createAfterYesterdayConstraint() { + return UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(today.minusDays(1).toString()) + .build()) + .build(); + } + + public static de.sovity.edc.client.gen.model.UiPolicyCreateRequest createAfterYesterdayPolicyEdcGen() { + return jsonCast(createAfterYesterdayPolicy(), de.sovity.edc.client.gen.model.UiPolicyCreateRequest.class); + } + + private static R jsonCast(T obj, Class clazz) { + return JSON.deserialize(JSON.serialize(obj), clazz); + } + + public static UiPolicyCreateRequest createAfterYesterdayPolicy() { + return UiPolicyCreateRequest.builder() + .constraints(List.of(createAfterYesterdayConstraint())) + .build(); + } + + public static JSONB createAfterYesterdayPolicyJson() { + var createRequest = TestPolicy.createAfterYesterdayPolicy(); + return getPolicyJsonLd(createRequest); + } + + /** + * This method only works in integration tests, because it depends on the broker server extension context. + */ + public static JSONB getPolicyJsonLd(UiPolicyCreateRequest createRequest) { + var policyMapper = BrokerServerExtensionContext.instance.policyMapper(); + var jsonLd = policyMapper.buildPolicyJsonLd(policyMapper.buildPolicy(createRequest)); + return JSONB.jsonb(JsonUtils.toJson(jsonLd)); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index c9e14605c..2d797c6c6 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -19,7 +19,6 @@ import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import org.assertj.core.api.ThrowableAssert; -import org.eclipse.edc.protocol.ids.api.configuration.IdsApiConfigurationExtension; import org.jetbrains.annotations.NotNull; import java.util.HashMap; @@ -31,19 +30,19 @@ import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; public class TestUtils { - - private static final int DATA_PORT = getFreePort(); + private static final int MANAGEMENT_PORT = getFreePort(); private static final int PROTOCOL_PORT = getFreePort(); - private static final String DATA_PATH = "/api/v1/data"; - private static final String PROTOCOL_PATH = "/api/v1/ids"; + private static final String MANAGEMENT_PATH = "/api/management"; + private static final String PROTOCOL_PATH = "/api/dsp"; public static final String MANAGEMENT_API_KEY = "123456"; - public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + DATA_PORT + DATA_PATH; - + public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + MANAGEMENT_PORT + MANAGEMENT_PATH; public static final String ADMIN_API_KEY = "123456"; public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; - public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH + "/data"; + public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH; + public static final String PARTICIPANT_ID = "my-edc-participant-id"; + public static final String CURATOR_NAME = "My Org"; @NotNull public static Map createConfiguration( @@ -51,16 +50,26 @@ public static Map createConfiguration( Map additionalConfigProperties ) { Map config = new HashMap<>(); + config.put("web.http.port", String.valueOf(getFreePort())); config.put("web.http.path", "/api"); - config.put("web.http.management.port", String.valueOf(DATA_PORT)); - config.put("web.http.management.path", DATA_PATH); + config.put("web.http.management.port", String.valueOf(MANAGEMENT_PORT)); + config.put("web.http.management.path", MANAGEMENT_PATH); config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); config.put("web.http.protocol.path", PROTOCOL_PATH); config.put("edc.api.auth.key", MANAGEMENT_API_KEY); - config.put("edc.ids.endpoint", PROTOCOL_ENDPOINT); - config.put(IdsApiConfigurationExtension.IDS_WEBHOOK_ADDRESS, PROTOCOL_HOST); + config.put("edc.dsp.callback.address", PROTOCOL_ENDPOINT); config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); + + config.put("edc.participant.id", PARTICIPANT_ID); + config.put("my.edc.name.kebab.case", PARTICIPANT_ID); + config.put("my.edc.title", "My Connector"); + config.put("my.edc.description", "My Connector Description"); + config.put("my.edc.curator.url", "https://connector.my-org"); + config.put("my.edc.curator.name", CURATOR_NAME); + config.put("my.edc.maintainer.url", "https://maintainer-org"); + config.put("my.edc.maintainer.name", "Maintainer Org"); + config.put(PostgresFlywayExtension.JDBC_URL, testDatabase.getJdbcUrl()); config.put(PostgresFlywayExtension.JDBC_USER, testDatabase.getJdbcUser()); config.put(PostgresFlywayExtension.JDBC_PASSWORD, testDatabase.getJdbcPassword()); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java index bf200c29d..e593df41c 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java @@ -19,8 +19,8 @@ import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; -import java.util.function.Consumer; import javax.sql.DataSource; +import java.util.function.Consumer; public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { String getJdbcUrl(); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index b5c1b9b86..4b0bf063c 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -25,13 +25,15 @@ import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValueAttribute; import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageResult; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.JsonObject; import lombok.SneakyThrows; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -48,6 +50,8 @@ import java.util.List; import java.util.Map; +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; +import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static java.util.stream.IntStream.range; @@ -65,7 +69,7 @@ void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=http://my-connector2/ids/data,Example2=http://my-connector3/ids/data" + BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=https://my-connector2/api/dsp,Example2=https://my-connector3/api/dsp" ))); } @@ -75,16 +79,15 @@ void testDataSpace_two_dataspaces_filter_for_one() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: MDS - createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector/ids/data"); // Dataspace: MDS - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector2/ids/data"); // Dataspace: Example1 + var assetJsonLd = getAssetJsonLd("my-asset", Map.of(Prop.Dcterms.TITLE, "My Asset")); + + // Dataspace: MDS + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); + + // Dataspace: Example1 + createConnector(dsl, today, "https://my-connector2/api/dsp"); + createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd); var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( @@ -95,7 +98,7 @@ void testDataSpace_two_dataspaces_filter_for_one() { assertThat(result.getDataOffers()).hasSize(1); var dataOfferResult = result.getDataOffers().get(0); - assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("http://my-connector2/ids/data"); + assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("https://my-connector2/api/dsp"); }); } @@ -105,24 +108,21 @@ void testConnectorEndpointFilter_two_connectors_filter_for_one() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createConnector(dsl, today, "http://my-connector2/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector2/ids/data"); + var assetJsonLd = getAssetJsonLd("my-asset", Map.of(Prop.Dcterms.TITLE, "My Asset")); + + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); + + createConnector(dsl, today, "https://my-connector2/api/dsp"); + createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd); var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("connectorEndpoint", List.of("http://my-connector/ids/data")) + new CnfFilterValueAttribute("connectorEndpoint", List.of("https://my-connector/api/dsp")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly("urn:artifact:my-asset"); + assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly("https://my-connector/api/dsp"); }); } @@ -132,29 +132,18 @@ void test_available_filter_values_to_filter_by() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); // Dataspace: MDS - createConnector(dsl, today, "http://my-connector2/ids/data"); // Dataspace: Example1 - createConnector(dsl, today, "http://my-connector3/ids/data"); // Dataspace: Example2 - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "de" - ), "http://my-connector/ids/data"); // Dataspace: MDS - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "en" - ), "http://my-connector2/ids/data"); // Dataspace: Example1 - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset2", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" - ), "http://my-connector2/ids/data"); // Dataspace: Example1 - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset3", - AssetProperty.ASSET_NAME, "my-asset", - AssetProperty.LANGUAGE, "fr" - ), "http://my-connector3/ids/data"); // Dataspace: Example2 + createConnector(dsl, today, "https://my-connector/api/dsp"); // Dataspace: MDS + createConnector(dsl, today, "https://my-connector2/api/dsp"); // Dataspace: Example1 + createConnector(dsl, today, "https://my-connector3/api/dsp"); // Dataspace: Example2 + + var assetJsonLd1 = getAssetJsonLd("my-asset-1"); + var assetJsonLd2 = getAssetJsonLd("my-asset-2"); + var assetJsonLd3 = getAssetJsonLd("my-asset-3"); + + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); // Dataspace: MDS + createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd1); // Dataspace: Example1 + createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd2); // Dataspace: Example1 + createDataOffer(dsl, today, "https://my-connector3/api/dsp", assetJsonLd3); // Dataspace: Example2 // get all available filter values var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -175,25 +164,23 @@ void testDataOfferDetails() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" - ), "http://my-connector/ids/data"); + var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( + Prop.Dcterms.TITLE, "My Asset" + )); + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); assertThat(result.getDataOffers()).hasSize(1); var dataOfferResult = result.getDataOffers().get(0); - assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("https://my-connector/api/dsp"); assertThat(dataOfferResult.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(CatalogDataOffer.ConnectorOnlineStatusEnum.ONLINE); - assertThat(dataOfferResult.getAssetId()).isEqualTo("urn:artifact:my-asset"); - assertThat(dataOfferResult.getProperties()).isEqualTo(Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset", - AssetProperty.ASSET_NAME, "my-asset" - )); + assertThat(dataOfferResult.getAssetId()).isEqualTo("my-asset-1"); + assertThat(dataOfferResult.getAsset().getAssetId()).isEqualTo("my-asset-1"); + assertThat(dataOfferResult.getAsset().getTitle()).isEqualTo("My Asset"); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); }); } @@ -206,7 +193,7 @@ void testEmptyConnector() { TEST_DATABASE.testTransaction(dsl -> { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); + createConnector(dsl, today, "https://my-connector/api/dsp"); // act var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -226,31 +213,39 @@ void testAvailableFilters_noFilter() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" - ), "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "my-transport-mode-2", - AssetProperty.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" - ), "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-3", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - AssetProperty.DATA_SUBCATEGORY, "my-subcategory-1" - ), "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-4", - AssetProperty.DATA_CATEGORY, "my-category-1", - AssetProperty.TRANSPORT_MODE, "" - ), "http://my-connector/ids/data"); + var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", + Prop.Mds.DATA_MODEL, "my-data-model", + Prop.Mds.GEO_REFERENCE_METHOD, "my-geo-ref", + Prop.Dcterms.CREATOR, Map.of( + Prop.Foaf.NAME, "my-org" + ) + )); + + var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "my-transport-mode-2", + Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + )); + + var assetJsonLd3 = getAssetJsonLd("my-asset-3", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + Prop.Mds.DATA_SUBCATEGORY, "my-subcategory-1" + )); + + var assetJsonLd4 = getAssetJsonLd("my-asset-4", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "" + )); + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd2); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd3); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd4); var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -258,11 +253,12 @@ void testAvailableFilters_noFilter() { .extracting(CnfFilterAttribute::getId) .containsExactly( "dataSpace", - AssetProperty.DATA_CATEGORY, - AssetProperty.DATA_SUBCATEGORY, - AssetProperty.DATA_MODEL, - AssetProperty.TRANSPORT_MODE, - AssetProperty.GEO_REFERENCE_METHOD, + "dataCategory", + "dataSubcategory", + "dataModel", + "transportMode", + "geoReferenceMethod", + "curatorOrganizationName", "connectorEndpoint" ); @@ -275,28 +271,41 @@ void testAvailableFilters_noFilter() { "Data Model", "Transport Mode", "Geo Reference Method", + "Organization Name", "Connector" ); - var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); - assertThat(dataCategory.getTitle()).isEqualTo("Data Category"); + var dataSpace = getAvailableFilter(result, "dataSpace"); + assertThat(dataSpace.getValues()).extracting(CnfFilterItem::getId).containsExactly("MDS"); + assertThat(dataSpace.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MDS"); + + var dataCategory = getAvailableFilter(result, "dataCategory"); assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-category-1"); assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-category-1"); - var transportMode = getAvailableFilter(result, AssetProperty.TRANSPORT_MODE); - assertThat(transportMode.getTitle()).isEqualTo("Transport Mode"); + var dataSubcategory = getAvailableFilter(result, "dataSubcategory"); + assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); + assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); + + var dataModel = getAvailableFilter(result, "dataModel"); + assertThat(dataModel.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-data-model", ""); + assertThat(dataModel.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-data-model", ""); + + var transportMode = getAvailableFilter(result, "transportMode"); assertThat(transportMode.getValues()).extracting(CnfFilterItem::getId).containsExactly("MY-TRANSPORT-MODE-1", "my-transport-mode-2", ""); assertThat(transportMode.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MY-TRANSPORT-MODE-1", "my-transport-mode-2", ""); - var dataSubcategory = getAvailableFilter(result, AssetProperty.DATA_SUBCATEGORY); - assertThat(dataSubcategory.getTitle()).isEqualTo("Data Subcategory"); - assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); - assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); + var geoReferenceMethod = getAvailableFilter(result, "geoReferenceMethod"); + assertThat(geoReferenceMethod.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-geo-ref", ""); + assertThat(geoReferenceMethod.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-geo-ref", ""); + + var curatorOrganizationName = getAvailableFilter(result, "curatorOrganizationName"); + assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-org", "my-participant-id"); // second value comes from tests mocking + assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-org", "my-participant-id"); var connectorEndpoint = getAvailableFilter(result, "connectorEndpoint"); - assertThat(connectorEndpoint.getTitle()).isEqualTo("Connector"); - assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getId).containsExactly("http://my-connector/ids/data"); - assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("http://my-connector/ids/data"); + assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getId).containsExactly("https://my-connector/api/dsp"); + assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("https://my-connector/api/dsp"); }); } @@ -312,11 +321,10 @@ void testSearchCaseInsensitive() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "123", - AssetProperty.ASSET_NAME, "Hello" - ), "http://my-connector/ids/data"); + var assetJsonLd = getAssetJsonLd("123", Map.of(Prop.Dcterms.TITLE, "Hello")); + + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); // act @@ -341,34 +349,31 @@ void testAvailableFilters_withFilter() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.DATA_SUBCATEGORY, "my-subcategory" - ), "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_SUBCATEGORY, "my-other-subcategory" - ), "http://my-connector/ids/data"); + var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Mds.DATA_SUBCATEGORY, "my-subcategory" + )); + + var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( + Prop.Mds.DATA_SUBCATEGORY, "my-other-subcategory" + )); + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd2); var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute(AssetProperty.DATA_CATEGORY, List.of("")) + new CnfFilterValueAttribute("dataCategory", List.of("")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); - var dataCategory = getAvailableFilter(result, AssetProperty.DATA_CATEGORY); - assertThat(dataCategory.getId()).isEqualTo(AssetProperty.DATA_CATEGORY); - assertThat(dataCategory.getTitle()).isEqualTo("Data Category"); + var dataCategory = getAvailableFilter(result, "dataCategory"); assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-category", ""); assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-category", ""); - var dataSubcategory = getAvailableFilter(result, AssetProperty.DATA_SUBCATEGORY); - assertThat(dataSubcategory.getId()).isEqualTo(AssetProperty.DATA_SUBCATEGORY); - assertThat(dataSubcategory.getTitle()).isEqualTo("Data Subcategory"); + var dataSubcategory = getAvailableFilter(result, "dataSubcategory"); assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-other-subcategory"); assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-other-subcategory"); }); @@ -380,14 +385,9 @@ void testPagination_firstPage() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) - ), "http://my-connector/ids/data")); - range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) - ), "http://my-connector/ids/data")); - + createConnector(dsl, today, "https://my-connector/api/dsp"); + range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("my-asset-%d".formatted(i)))); + range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("some-other-asset-%d".formatted(i)))); var query = new CatalogPageQuery(); query.setSearchQuery("my-asset"); @@ -395,7 +395,7 @@ void testPagination_firstPage() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(range(0, 10).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + .isEqualTo(range(0, 10).mapToObj("my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(1); @@ -411,13 +411,9 @@ void testPagination_secondPage() { // arrange var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(i) - ), "http://my-connector/ids/data")); - range(0, 15).forEach(i -> createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:some-other-asset-%d".formatted(i) - ), "http://my-connector/ids/data")); + createConnector(dsl, today, "https://my-connector/api/dsp"); + range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("my-asset-%d".formatted(i)))); + range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("some-other-asset-%d".formatted(i)))); var query = new CatalogPageQuery(); @@ -428,7 +424,7 @@ void testPagination_secondPage() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(range(10, 15).mapToObj("urn:artifact:my-asset-%d"::formatted).toList()); + .isEqualTo(range(10, 15).mapToObj("my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(2); @@ -444,14 +440,14 @@ void testSortingByPopularity() { // arrange var today = OffsetDateTime.now().withNano(0); - var endpoint = "http://my-connector/ids/data"; + var endpoint = "https://my-connector/api/dsp"; createConnector(dsl, today, endpoint); - createDataOffer(dsl, today, Map.of(AssetProperty.ASSET_ID, "urn:artifact:asset-1"), endpoint); - createDataOffer(dsl, today, Map.of(AssetProperty.ASSET_ID, "urn:artifact:asset-2"), endpoint); - createDataOffer(dsl, today, Map.of(AssetProperty.ASSET_ID, "urn:artifact:asset-3"), endpoint); + createDataOffer(dsl, today, endpoint, getAssetJsonLd("asset-1")); + createDataOffer(dsl, today, endpoint, getAssetJsonLd("asset-2")); + createDataOffer(dsl, today, endpoint, getAssetJsonLd("asset-3")); - range(0, 3).forEach(i -> dataOfferDetails(endpoint, "urn:artifact:asset-1")); - range(0, 5).forEach(i -> dataOfferDetails(endpoint, "urn:artifact:asset-2")); + range(0, 3).forEach(i -> dataOfferDetails(endpoint, "asset-1")); + range(0, 5).forEach(i -> dataOfferDetails(endpoint, "asset-2")); var query = new CatalogPageQuery(); @@ -459,27 +455,25 @@ void testSortingByPopularity() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly( - "urn:artifact:asset-2", - "urn:artifact:asset-1", - "urn:artifact:asset-3" + "asset-2", + "asset-1", + "asset-3" ); }); } - private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { + private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); - dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); - dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); dataOffer.setConnectorEndpoint(connectorEndpoint); dataOffer.setCreatedAt(today.minusDays(5)); dataOffer.setUpdatedAt(today); dataOffer.insert(); - var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); contractOffer.setContractOfferId("my-contract-offer-1"); contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setAssetId(dataOffer.getAssetId()); contractOffer.setCreatedAt(today.minusDays(5)); contractOffer.setUpdatedAt(today); contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); @@ -488,7 +482,7 @@ private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties) { - return new ObjectMapper().writeValueAsString(assetProperties); - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index 1436ffd18..c567728af 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -14,10 +14,9 @@ package de.sovity.edc.ext.brokerserver.services.api; -import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.brokerserver.TestPolicy; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorDetailPageQuery; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; @@ -27,12 +26,11 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import lombok.SneakyThrows; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.JsonObject; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.policy.model.Policy; import org.jooq.DSLContext; -import org.jooq.JSONB; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,9 +39,10 @@ import java.time.OffsetDateTime; import java.util.Map; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; +import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static groovy.json.JsonOutput.toJson; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -55,8 +54,7 @@ class ConnectorApiTest { @BeforeEach void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - ))); + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); } @Test @@ -64,22 +62,24 @@ void testQueryConnectors() { TEST_DATABASE.testTransaction(dsl -> { var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" - ), "http://my-connector/ids/data"); + var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Dcterms.TITLE, "My Asset 1" + )); + + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); var result = brokerServerClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); assertThat(result.getConnectors()).hasSize(1); var connector = result.getConnectors().get(0); - assertThat(connector.getId()).isEqualTo("http://my-connector/ids/data"); - assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); + assertThat(connector.getParticipantId()).isEqualTo("my-participant-id"); + assertThat(connector.getEndpoint()).isEqualTo("https://my-connector/api/dsp"); assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); + assertThat(connector.getNumDataOffers()).isEqualTo(1); }); } @@ -88,17 +88,18 @@ void testQueryConnectorDetails() { TEST_DATABASE.testTransaction(dsl -> { var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector/ids/data"); - createConnector(dsl, today, "http://my-connector2/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" - ), "http://my-connector/ids/data"); - - var connector = brokerServerClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("http://my-connector/ids/data")); - assertThat(connector.getId()).isEqualTo("http://my-connector/ids/data"); - assertThat(connector.getEndpoint()).isEqualTo("http://my-connector/ids/data"); + var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Dcterms.TITLE, "My Asset 1" + )); + + createConnector(dsl, today, "https://my-connector/api/dsp"); + createConnector(dsl, today, "https://my-connector2/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); + + var connector = brokerServerClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("https://my-connector/api/dsp")); + assertThat(connector.getParticipantId()).isEqualTo("my-participant-id"); + assertThat(connector.getEndpoint()).isEqualTo("https://my-connector/api/dsp"); assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); @@ -108,7 +109,7 @@ void testQueryConnectorDetails() { private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setConnectorId(connectorEndpoint); + connector.setParticipantId("my-participant-id"); connector.setEndpoint(connectorEndpoint); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setCreatedAt(today.minusDays(1)); @@ -132,38 +133,21 @@ private static void addCrawlingTime(DSLContext dsl, OffsetDateTime today, Connec crawlingTime.insert(); } - private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { + private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); - dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); - dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); dataOffer.setConnectorEndpoint(connectorEndpoint); dataOffer.setCreatedAt(today.minusDays(5)); dataOffer.setUpdatedAt(today); dataOffer.insert(); - var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); contractOffer.setContractOfferId("my-contract-offer-1"); contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setAssetId(dataOffer.getAssetId()); contractOffer.setCreatedAt(today.minusDays(5)); contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); contractOffer.insert(); } - - private Policy dummyPolicy() { - return Policy.Builder.newInstance() - .assignee("Example Assignee") - .build(); - } - - private String policyToJson(Policy policy) { - return toJson(policy); - } - - @SneakyThrows - private String assetProperties(Map assetProperties) { - return new ObjectMapper().writeValueAsString(assetProperties); - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java index 053c14c6d..722ee6747 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java @@ -14,20 +14,18 @@ package de.sovity.edc.ext.brokerserver.services.api; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.TestPolicy; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.SneakyThrows; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.policy.model.Policy; import org.jooq.DSLContext; -import org.jooq.JSONB; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,6 +35,8 @@ import java.util.Arrays; import java.util.Map; +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; +import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static groovy.json.JsonOutput.toJson; @@ -88,7 +88,7 @@ void testCountByEndpoints() { private void createConnector(DSLContext dsl, OffsetDateTime today, int iConnector) { var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setConnectorId("https://my-connector"); + connector.setParticipantId("my-connector"); connector.setEndpoint(getEndpoint(iConnector)); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setCreatedAt(today.minusDays(1)); @@ -105,26 +105,22 @@ private String getEndpoint(int iConnector) { private void createDataOffer(DSLContext dsl, OffsetDateTime today, int iConnector, int iDataOffer) { var connectorEndpoint = getEndpoint(iConnector); - var assetProperties = Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-%d".formatted(iDataOffer) - ); + var assetJsonLd = getAssetJsonLd("my-asset-%d".formatted(iDataOffer)); var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); - dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); - dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); dataOffer.setConnectorEndpoint(connectorEndpoint); dataOffer.setCreatedAt(today.minusDays(5)); dataOffer.setUpdatedAt(today); dataOffer.insert(); - var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); contractOffer.setContractOfferId("my-contract-offer-1"); contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setAssetId(dataOffer.getAssetId()); contractOffer.setCreatedAt(today.minusDays(5)); contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); contractOffer.insert(); } @@ -137,9 +133,4 @@ private Policy dummyPolicy() { private String policyToJson(Policy policy) { return toJson(policy); } - - @SneakyThrows - private String assetProperties(Map assetProperties) { - return new ObjectMapper().writeValueAsString(assetProperties); - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index 44e72ded3..299742ba7 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -14,22 +14,19 @@ package de.sovity.edc.ext.brokerserver.services.api; -import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageResult; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.SneakyThrows; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.JsonObject; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.policy.model.Policy; import org.jooq.DSLContext; -import org.jooq.JSONB; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,10 +35,13 @@ import java.time.OffsetDateTime; import java.util.Map; -import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualUsingJson; +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; +import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; +import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayConstraint; +import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayPolicyJson; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static groovy.json.JsonOutput.toJson; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -62,48 +62,39 @@ void testQueryDataOfferDetails() { TEST_DATABASE.testTransaction(dsl -> { var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "http://my-connector2/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-2", - AssetProperty.DATA_CATEGORY, "my-category2", - AssetProperty.ASSET_NAME, "My Asset 2" - ), "http://my-connector2/ids/data"); - - createConnector(dsl, today, "http://my-connector/ids/data"); - createDataOffer(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" - ), "http://my-connector/ids/data"); - - //create view for dataoffer - createDataOfferView(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" - ), "http://my-connector/ids/data"); - createDataOfferView(dsl, today, Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" - ), "http://my-connector/ids/data"); - - var actual = brokerServerClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("http://my-connector/ids/data", "urn:artifact:my-asset-1")); - assertThat(actual.getAssetId()).isEqualTo("urn:artifact:my-asset-1"); - assertThat(actual.getConnectorEndpoint()).isEqualTo("http://my-connector/ids/data"); + var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Dcterms.TITLE, "My Asset 1" + )); + + var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( + Prop.Mds.DATA_CATEGORY, "my-category-2", + Prop.Dcterms.TITLE, "My Asset 2" + )); + + createConnector(dsl, today, "https://my-connector/api/dsp"); + createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); + + createDataOfferView(dsl, today, "https://my-connector/api/dsp", "my-asset-1"); + createDataOfferView(dsl, today, "https://my-connector/api/dsp", "my-asset-1"); + + createConnector(dsl, today, "https://my-connector2/api/dsp"); + createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd2); + + var actual = brokerServerClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("https://my-connector/api/dsp", "my-asset-1")); + assertThat(actual.getAssetId()).isEqualTo("my-asset-1"); + assertThat(actual.getConnectorEndpoint()).isEqualTo("https://my-connector/api/dsp"); assertThat(actual.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); assertThat(actual.getConnectorOnlineStatus()).isEqualTo(DataOfferDetailPageResult.ConnectorOnlineStatusEnum.ONLINE); assertThat(actual.getCreatedAt()).isEqualTo(today.minusDays(5)); - assertThat(actual.getProperties()).isEqualTo(Map.of( - AssetProperty.ASSET_ID, "urn:artifact:my-asset-1", - AssetProperty.DATA_CATEGORY, "my-category", - AssetProperty.ASSET_NAME, "My Asset 1" - )); + assertThat(actual.getAsset().getAssetId()).isEqualTo("my-asset-1"); + assertThat(actual.getAsset().getDataCategory()).isEqualTo("my-category"); + assertThat(actual.getAsset().getTitle()).isEqualTo("My Asset 1"); assertThat(actual.getUpdatedAt()).isEqualTo(today); assertThat(actual.getContractOffers()).hasSize(1); var contractOffer = actual.getContractOffers().get(0); assertThat(contractOffer.getContractOfferId()).isEqualTo("my-contract-offer-1"); - assertEqualJson(contractOffer.getContractPolicy().getLegacyPolicy(), policyToJson(dummyPolicy())); + assertEqualUsingJson(contractOffer.getContractPolicy().getConstraints().get(0), createAfterYesterdayConstraint()); assertThat(contractOffer.getCreatedAt()).isEqualTo(today.minusDays(5)); assertThat(contractOffer.getUpdatedAt()).isEqualTo(today); assertThat(actual.getViewCount()).isEqualTo(2); @@ -112,7 +103,7 @@ void testQueryDataOfferDetails() { private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setConnectorId("http://my-connector"); + connector.setParticipantId("my-connector"); connector.setEndpoint(connectorEndpoint); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setCreatedAt(today.minusDays(1)); @@ -123,46 +114,29 @@ private void createConnector(DSLContext dsl, OffsetDateTime today, String connec connector.insert(); } - private void createDataOffer(DSLContext dsl, OffsetDateTime today, Map assetProperties, String connectorEndpoint) { + private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - dataOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); - dataOffer.setAssetName(assetProperties.getOrDefault(AssetProperty.ASSET_NAME, dataOffer.getAssetId())); - dataOffer.setAssetProperties(JSONB.jsonb(assetProperties(assetProperties))); + setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); dataOffer.setConnectorEndpoint(connectorEndpoint); dataOffer.setCreatedAt(today.minusDays(5)); dataOffer.setUpdatedAt(today); dataOffer.insert(); - var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); contractOffer.setContractOfferId("my-contract-offer-1"); contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + contractOffer.setAssetId(dataOffer.getAssetId()); contractOffer.setCreatedAt(today.minusDays(5)); contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); + contractOffer.setPolicy(createAfterYesterdayPolicyJson()); contractOffer.insert(); } - private static void createDataOfferView(DSLContext dsl, OffsetDateTime date, Map assetProperties, String connectorEndpoint) { + private static void createDataOfferView(DSLContext dsl, OffsetDateTime date, String connectorEndpoint, String assetId) { var view = dsl.newRecord(Tables.DATA_OFFER_VIEW_COUNT); - view.setAssetId(assetProperties.get(AssetProperty.ASSET_ID)); + view.setAssetId(assetId); view.setConnectorEndpoint(connectorEndpoint); view.setDate(date); view.insert(); } - - private Policy dummyPolicy() { - return Policy.Builder.newInstance() - .assignee("Example Assignee") - .build(); - } - - private String policyToJson(Policy policy) { - return toJson(policy); - } - - @SneakyThrows - private String assetProperties(Map assetProperties) { - return new ObjectMapper().writeValueAsString(assetProperties); - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java index 21c57b658..fb6ba3aca 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.brokerserver.TestPolicy; import de.sovity.edc.ext.brokerserver.TestUtils; import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; @@ -28,9 +29,7 @@ import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; import org.jooq.DSLContext; -import org.jooq.JSONB; import org.jooq.Record; -import org.jooq.Record1; import org.jooq.TableField; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +41,8 @@ import java.util.List; import java.util.Map; +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; +import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; @@ -72,7 +73,7 @@ void testRemoveConnectors() { var connectorsBefore = List.of(firstConnector, otherConnector); assertContainsEndpoints(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, connectorsBefore); - assertContainsEndpoints(dsl, Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); + assertContainsEndpoints(dsl, Tables.CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); assertContainsEndpoints(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); assertContainsEndpoints(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, connectorsBefore); assertContainsEndpoints(dsl, Tables.CONNECTOR.ENDPOINT, connectorsBefore); @@ -87,7 +88,7 @@ void testRemoveConnectors() { var connectorsAfter = List.of(otherConnector); assertContainsEndpoints(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, connectorsAfter); - assertContainsEndpoints(dsl, Tables.DATA_OFFER_CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); + assertContainsEndpoints(dsl, Tables.CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); assertContainsEndpoints(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); assertContainsEndpoints(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, connectorsAfter); assertContainsEndpoints(dsl, Tables.CONNECTOR.ENDPOINT, connectorsAfter); @@ -109,20 +110,18 @@ public void setupConnectorData(DSLContext dsl, String endpoint) { var assetId = "my-asset"; var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - dataOffer.setAssetId(assetId); - dataOffer.setAssetName("My Asset"); - dataOffer.setAssetProperties(JSONB.valueOf("{}")); + setDataOfferAssetMetadata(dataOffer, getAssetJsonLd("my-asset"), "my-participant-id"); dataOffer.setConnectorEndpoint(endpoint); dataOffer.setCreatedAt(OffsetDateTime.now()); dataOffer.setUpdatedAt(OffsetDateTime.now()); dataOffer.insert(); - var contractOffer = dsl.newRecord(Tables.DATA_OFFER_CONTRACT_OFFER); + var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); contractOffer.setAssetId(assetId); contractOffer.setConnectorEndpoint(endpoint); contractOffer.setContractOfferId("my-asset-co"); contractOffer.setCreatedAt(OffsetDateTime.now()); - contractOffer.setPolicy(JSONB.valueOf("{}")); + contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); contractOffer.setUpdatedAt(OffsetDateTime.now()); contractOffer.insert(); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java index d93c33671..eee984c6f 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java @@ -42,7 +42,7 @@ void testDataOfferWriter_allSortsOfUpdates() { var brokerEventLogger = new BrokerEventLogger(); // Test that insertions insert required fields and don't cause DB errors - String endpoint = "https://example.com/ids/data"; + String endpoint = "https://example.com/api/dsp"; brokerEventLogger.logConnectorUpdated(dsl, endpoint, new ConnectorChangeTracker()); brokerEventLogger.logConnectorOnline(dsl, endpoint); brokerEventLogger.logConnectorOffline(dsl, endpoint, new BrokerEventErrorMessage("Message", "Stacktrace")); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index 3dcd46eaa..07ac1e531 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -14,184 +14,171 @@ package de.sovity.edc.ext.brokerserver.services.refreshing; +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.ext.brokerserver.AssertionUtils; import de.sovity.edc.ext.brokerserver.BrokerServerExtensionContext; import de.sovity.edc.ext.brokerserver.TestUtils; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; +import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; +import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import io.restassured.path.json.JsonPath; -import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; -import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; -import org.eclipse.edc.connector.policy.spi.PolicyDefinition; -import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; -import org.eclipse.edc.connector.spi.asset.AssetService; +import de.sovity.edc.utils.jsonld.vocab.Prop; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.policy.model.Policy; -import org.eclipse.edc.spi.asset.AssetSelectorExpression; -import org.eclipse.edc.spi.persistence.EdcPersistenceException; -import org.eclipse.edc.spi.types.domain.DataAddress; -import org.eclipse.edc.spi.types.domain.asset.Asset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayConstraint; +import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayPolicyEdcGen; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; @ApiTest -@ExtendWith(EdcExtension.class) class ConnectorUpdaterTest { @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + private EdcClient providerClient; + + private BrokerServerClient brokerServerClient; + @BeforeEach void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + + providerClient = EdcClient.builder() + .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) + .managementApiKey(TestUtils.MANAGEMENT_API_KEY) + .build(); + + brokerServerClient = BrokerServerClient.builder() + .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) + .managementApiKey(TestUtils.MANAGEMENT_API_KEY) + .build(); } @Test - void testConnectorUpdate( - AssetService assetService, - PolicyDefinitionStore policyDefinitionStore, - ContractDefinitionStore contractDefinitionStore - ) { + void testConnectorUpdate() { TEST_DATABASE.testTransaction(dsl -> { // arrange - var connectorEndpoint = TestUtils.PROTOCOL_ENDPOINT; var connectorUpdater = BrokerServerExtensionContext.instance.connectorUpdater(); var connectorCreator = BrokerServerExtensionContext.instance.connectorCreator(); + String connectorEndpoint = TestUtils.PROTOCOL_ENDPOINT; - createAlwaysTruePolicyDefinition(policyDefinitionStore); - createAlwaysTrueContractDefinition(contractDefinitionStore); - - var nestedObjProperty = new LinkedHashMap(Map.of( - "test-string", "hello", - "test-uri", "https://w3id.org/idsa/code/AB", - "http://test-uri-key", "value", - - "test-array", new ArrayList<>(List.of("a", "b")), - "test-float", 5.1, - "test-int", 5, - "test-boolean", true, - - "test-obj", new LinkedHashMap<>(Map.of("key", "value")) - )); - nestedObjProperty.put("test-null", null); - - var asset = Asset.Builder.newInstance() - .id("test-asset-1") - .property(AssetProperty.ASSET_ID, "test-asset-1") - .property(AssetProperty.ASSET_NAME, "Test Asset 1") - .property("test-string", "hello") - .property("test-uri", "https://w3id.org/idsa/code/AB") - .property("http://test-uri-key", "value") - - .property("test-array", new ArrayList<>(List.of("a", "b"))) - .property("test-float", 5.1) - .property("test-int", 5) - .property("test-boolean", true) - - .property("test-obj", nestedObjProperty) - .build(); - - assetService.create(asset, dataAddress()); + var policyId = createPolicyDefinition(); + var assetId = createAsset(); + createContractDefinition(policyId, assetId); connectorCreator.addConnector(dsl, connectorEndpoint); // act connectorUpdater.updateConnector(connectorEndpoint); // assert - var connectors = dsl.selectFrom(Tables.CONNECTOR).stream().toList(); - assertThat(connectors.get(0).getOnlineStatus()).isEqualTo(ConnectorOnlineStatus.ONLINE); - assertThat(connectors.get(0).getEndpoint()).isEqualTo(connectorEndpoint); - - var dataOffers = dsl.selectFrom(Tables.DATA_OFFER).stream().toList(); - assertThat(dataOffers).hasSize(1); - - var dataOffer = dataOffers.get(0); - assertThat(dataOffer.getAssetId()).isEqualTo("test-asset-1"); - assertThat(dataOffer.getAssetProperties().data()).contains("Test Asset 1"); - - var props = JsonPath.from(dataOffer.getAssetProperties().data()); - assertThat(props.getString("\"asset:prop:name\"")).isEqualTo("Test Asset 1"); - assertThat(props.getString("test-string")).isEqualTo("hello"); - assertThat(props.getString("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); - assertThat(props.getString("http://test-uri-key")).isEqualTo("value"); - - assertThat(props.getString("test-array")).isEqualTo("[\"a\",\"b\"]"); - assertThat(props.getString("test-int")).isEqualTo("5"); - assertThat(props.getString("test-float")).isEqualTo("5.1"); - assertThat(props.getString("test-boolean")).isEqualTo("true"); - - var testObj = JsonPath.from(props.getString("test-obj")); - assertThat((String) testObj.get("test-string")).isEqualTo("hello"); - assertThat((String) testObj.get("test-uri")).isEqualTo("https://w3id.org/idsa/code/AB"); - assertThat((String) testObj.get("http://test-uri-key")).isEqualTo("value"); - - assertThat((List) testObj.get("test-array")).isEqualTo(List.of("a", "b")); - assertThat((Integer) testObj.get("test-int")).isEqualTo(5); - assertThat((Float) testObj.get("test-float")).isEqualTo(5.1f); - assertThat((Boolean) testObj.get("test-boolean")).isTrue(); - assertThat((Map) testObj.get("test-obj")).isEqualTo(Map.of("key", "value")); - - // the nested object's null will have disappeared - assertThat(testObj.getMap("")).containsKey("test-string"); - assertThat(testObj.getMap("")).doesNotContainKey("test-null"); - - var contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().toList(); - assertThat(contractOffers).hasSize(1); + var catalog = brokerServerClient.brokerServerApi().catalogPage(new CatalogPageQuery()); + assertThat(catalog.getDataOffers()).hasSize(1); + var dataOffer = catalog.getDataOffers().get(0); + assertThat(dataOffer.getContractOffers()).hasSize(1); + var contractOffer = dataOffer.getContractOffers().get(0); + var asset = dataOffer.getAsset(); + assertThat(asset.getAssetId()).isEqualTo(assetId); + assertThat(asset.getTitle()).isEqualTo("AssetName"); + assertThat(asset.getConnectorEndpoint()).isEqualTo(TestUtils.PROTOCOL_ENDPOINT); + assertThat(asset.getParticipantId()).isEqualTo(TestUtils.PARTICIPANT_ID); + assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); + assertThat(asset.getDescription()).isEqualTo("AssetDescription"); + assertThat(asset.getVersion()).isEqualTo("1.0.0"); + assertThat(asset.getLanguage()).isEqualTo("en"); + assertThat(asset.getMediaType()).isEqualTo("application/json"); + assertThat(asset.getDataCategory()).isEqualTo("dataCategory"); + assertThat(asset.getDataSubcategory()).isEqualTo("dataSubcategory"); + assertThat(asset.getDataModel()).isEqualTo("dataModel"); + assertThat(asset.getGeoReferenceMethod()).isEqualTo("geoReferenceMethod"); + assertThat(asset.getTransportMode()).isEqualTo("transportMode"); + assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url"); + assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); + assertThat(asset.getCreatorOrganizationName()).isEqualTo(TestUtils.CURATOR_NAME); + assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage"); + assertThat(asset.getHttpDatasourceHintsProxyMethod()).isFalse(); + assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); + assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isFalse(); + assertThat(asset.getHttpDatasourceHintsProxyBody()).isFalse(); + assertThat(asset.getAdditionalProperties()) + .containsExactlyEntriesOf(Map.of("http://unknown/a", "x")); + assertThat(dataOffer.getAsset().getAdditionalJsonProperties()) + .containsExactlyEntriesOf(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")); + assertThat(dataOffer.getAsset().getPrivateProperties()).isEmpty(); + assertThat(dataOffer.getAsset().getPrivateJsonProperties()).isEmpty(); + var policy = contractOffer.getContractPolicy(); + assertThat(policy.getConstraints()).hasSize(1); + AssertionUtils.assertEqualUsingJson(policy.getConstraints().get(0), createAfterYesterdayConstraint()); }); } - @Test - void testTopLevelAssetPropertyCannotBeNull(AssetService assetService) { - var asset = Asset.Builder.newInstance() - .id("test-asset-1") - .property("test-null", null) + private String createPolicyDefinition() { + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId("policy-1") + .policy(createAfterYesterdayPolicyEdcGen()) .build(); - var dataAddress = dataAddress(); - assertThatThrownBy(() -> assetService.create(asset, dataAddress)) - .isInstanceOf(EdcPersistenceException.class) - .hasMessage("java.lang.NullPointerException: Cannot invoke \"Object.getClass()\" because the return value of \"java.util.Map$Entry.getValue()\" is null"); - } - private DataAddress dataAddress() { - return DataAddress.Builder.newInstance() - .properties(Map.of( - "type", "HttpData", - "baseUrl", "https://jsonplaceholder.typicode.com/todos/1" - )) - .build(); + return providerClient.uiApi().createPolicyDefinition(policyDefinition).getId(); } - private void createAlwaysTruePolicyDefinition(PolicyDefinitionStore policyDefinitionStore) { - var policyDefinition = PolicyDefinition.Builder.newInstance() - .id("always-true") - .policy(Policy.Builder.newInstance().build()) - .build(); - policyDefinitionStore.save(policyDefinition); + public void createContractDefinition(String policyId, String assetId) { + providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId("cd-1") + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId) + .build()) + .build())) + .build()); } - public void createAlwaysTrueContractDefinition(ContractDefinitionStore contractDefinitionStore) { - var contractDefinition = ContractDefinition.Builder.newInstance() - .id("always-true-cd") - .contractPolicyId("always-true") - .accessPolicyId("always-true") - .selectorExpression(AssetSelectorExpression.SELECT_ALL) - .validity(1000) //else throws "validity must be strictly positive" - .build(); - contractDefinitionStore.save(contractDefinition); + private String createAsset() { + return providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("asset-1") + .title("AssetName") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, "http://some.url" + )) + .additionalProperties(Map.of("http://unknown/a", "x")) + .additionalJsonProperties(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")) + .privateProperties(Map.of("http://unknown/a-private", "x-private")) + .privateJsonProperties(Map.of("http://unknown/b-private", "{\"http://unknown/c-private\":\"y-private\"}")) + .build()).getId(); } - } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java index 885ad6834..9a0da5f0c 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java @@ -19,8 +19,8 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; import org.jooq.DSLContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,7 +58,7 @@ void no_limit_and_two_dataofffers_and_contractoffer_should_not_limit() { when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); - myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + myDataOffer.setContractOffers(List.of(new FetchedContractOffer(), new FetchedContractOffer())); var dataOffers = List.of(myDataOffer, myDataOffer); // act @@ -104,7 +104,7 @@ void limit_one_and_two_dataoffers_should_result_to_one() { when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); - myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + myDataOffer.setContractOffers(List.of(new FetchedContractOffer(), new FetchedContractOffer())); var dataOffers = List.of(myDataOffer, myDataOffer); // act @@ -133,7 +133,7 @@ void verify_logConnectorUpdateDataOfferLimitExceeded() { when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); - myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + myDataOffer.setContractOffers(List.of(new FetchedContractOffer(), new FetchedContractOffer())); var dataOffers = List.of(myDataOffer, myDataOffer); // act @@ -157,7 +157,7 @@ void verify_logConnectorUpdateDataOfferLimitOk() { when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); - myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + myDataOffer.setContractOffers(List.of(new FetchedContractOffer(), new FetchedContractOffer())); var dataOffers = List.of(myDataOffer, myDataOffer); // act @@ -181,7 +181,7 @@ void verify_logConnectorUpdateContractOfferLimitExceeded() { when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); - myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + myDataOffer.setContractOffers(List.of(new FetchedContractOffer(), new FetchedContractOffer())); var dataOffers = List.of(myDataOffer, myDataOffer); // act @@ -205,7 +205,7 @@ void verify_logConnectorUpdateContractOfferLimitOk() { when(settings.getMaxContractOffersPerDataOffer()).thenReturn(maxContractOffers); var myDataOffer = new FetchedDataOffer(); - myDataOffer.setContractOffers(List.of(new FetchedDataOfferContractOffer(), new FetchedDataOfferContractOffer())); + myDataOffer.setContractOffers(List.of(new FetchedContractOffer(), new FetchedContractOffer())); var dataOffers = List.of(myDataOffer, myDataOffer); // act diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java index 78412a0bd..c8792e345 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java @@ -30,6 +30,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; +import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; import static org.assertj.core.api.Assertions.assertThat; class DataOfferWriterTest { @@ -56,7 +57,7 @@ void testDataOfferWriter_allSortsOfUpdates() { testData.fetched(unchanged); var fieldChangedExisting = Do.forName("fieldChanged"); - var fieldChangedFetched = fieldChangedExisting.withAssetName("changed"); + var fieldChangedFetched = fieldChangedExisting.withAssetTitle("changed"); testData.existing(fieldChangedExisting); testData.fetched(fieldChangedFetched); @@ -139,9 +140,9 @@ void testDataOfferWriter_allSortsOfUpdates() { } private void assertAssetPropertiesEqual(DataOfferWriterTestDataHelper testData, DataOfferRecord actual, Do expected) { - var actualAssetJson = actual.getAssetProperties().data(); + var actualAssetJson = actual.getAssetJsonLd().data(); var expectedAssetJson = testData.dummyAssetJson(expected); - assertThat(actualAssetJson).isEqualTo(expectedAssetJson); + assertEqualJson(actualAssetJson, expectedAssetJson); } private void assertPolicyEquals( diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java index 227c5a82f..312e83765 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java @@ -14,13 +14,14 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.AssetProperty; import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; +import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOfferContractOffer; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -29,16 +30,18 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Do; class DataOfferWriterTestDataHelper { - String connectorEndpoint = "https://example.com/ids/data"; + String connectorEndpoint = "https://example.com/api/dsp"; OffsetDateTime old = OffsetDateTime.now().withNano(0).withSecond(0).withMinute(0).withHour(0).minusDays(100); - List existingContractOffers = new ArrayList<>(); + List existingContractOffers = new ArrayList<>(); List existingDataOffers = new ArrayList<>(); List fetchedDataOffers = new ArrayList<>(); @@ -75,8 +78,8 @@ public void initialize(DSLContext dsl) { dsl.batchInsert(existingContractOffers).execute(); } - private DataOfferContractOfferRecord dummyContractOffer(Do dataOffer, Co contractOffer) { - var contractOfferRecord = new DataOfferContractOfferRecord(); + private ContractOfferRecord dummyContractOffer(Do dataOffer, Co contractOffer) { + var contractOfferRecord = new ContractOfferRecord(); contractOfferRecord.setConnectorEndpoint(connectorEndpoint); contractOfferRecord.setAssetId(dataOffer.getAssetId()); contractOfferRecord.setContractOfferId(contractOffer.getId()); @@ -87,13 +90,13 @@ private DataOfferContractOfferRecord dummyContractOffer(Do dataOffer, Co contrac } private DataOfferRecord dummyDataOffer(Do dataOffer) { - var assetName = Optional.of(dataOffer.getAssetName()).orElse(dataOffer.getAssetId()); + var assetName = Optional.of(dataOffer.getAssetTitle()).orElse(dataOffer.getAssetId()); var dataOfferRecord = new DataOfferRecord(); dataOfferRecord.setConnectorEndpoint(connectorEndpoint); dataOfferRecord.setAssetId(dataOffer.getAssetId()); - dataOfferRecord.setAssetName(assetName); - dataOfferRecord.setAssetProperties(JSONB.valueOf(dummyAssetJson(dataOffer))); + dataOfferRecord.setAssetTitle(assetName); + dataOfferRecord.setAssetJsonLd(JSONB.valueOf(dummyAssetJson(dataOffer))); dataOfferRecord.setCreatedAt(old); dataOfferRecord.setUpdatedAt(old); return dataOfferRecord; @@ -102,8 +105,8 @@ private DataOfferRecord dummyDataOffer(Do dataOffer) { private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { var fetchedDataOffer = new FetchedDataOffer(); fetchedDataOffer.setAssetId(dataOffer.getAssetId()); - fetchedDataOffer.setAssetName(dataOffer.getAssetName()); - fetchedDataOffer.setAssetPropertiesJson(dummyAssetJson(dataOffer)); + fetchedDataOffer.setAssetTitle(dataOffer.getAssetTitle()); + fetchedDataOffer.setAssetJsonLd(dummyAssetJson(dataOffer)); var contractOffersMapped = dataOffer.getContractOffers().stream().map(this::dummyFetchedContractOffer).collect(Collectors.toList()); fetchedDataOffer.setContractOffers(contractOffersMapped); @@ -112,10 +115,8 @@ private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { } public String dummyAssetJson(Do dataOffer) { - return "{\"%s\": \"%s\", \"%s\": \"%s\"}".formatted( - AssetProperty.ASSET_ID, dataOffer.getAssetId(), - AssetProperty.ASSET_NAME, dataOffer.getAssetName() - ); + var assetJsonLd = getAssetJsonLd(dataOffer.getAssetId(), Map.of(Prop.Dcterms.TITLE, dataOffer.getAssetTitle())); + return JsonUtils.toJson(assetJsonLd); } public String dummyPolicyJson(String policyValue) { @@ -125,8 +126,8 @@ public String dummyPolicyJson(String policyValue) { } @NotNull - private FetchedDataOfferContractOffer dummyFetchedContractOffer(Co it) { - var contractOffer = new FetchedDataOfferContractOffer(); + private FetchedContractOffer dummyFetchedContractOffer(Co it) { + var contractOffer = new FetchedContractOffer(); contractOffer.setContractOfferId(it.getId()); contractOffer.setPolicyJson(dummyPolicyJson(it.getPolicyValue())); return contractOffer; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java index ca7343a94..ab15daccb 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java @@ -29,7 +29,7 @@ static class Do { @With String assetId; @With - String assetName; + String assetTitle; @With List contractOffers; @@ -40,7 +40,7 @@ public Do withContractOffer(Co co) { } public static Do forName(String name) { - return new Do(name, name + " Name", List.of(new Co(name + " CO", name + " Policy"))); + return new Do(name, name + " Title", List.of(new Co(name + " CO", name + " Policy"))); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java index 962695078..d44383937 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; -import de.sovity.edc.ext.brokerserver.dao.DataOfferContractOfferQueries; +import de.sovity.edc.ext.brokerserver.dao.ContractOfferQueries; import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; import lombok.Value; @@ -27,11 +27,11 @@ class DataOfferWriterTestDydi { Config config = mock(Config.class); BrokerServerSettings brokerServerSettings = mock(BrokerServerSettings.class); DataOfferQueries dataOfferQueries = new DataOfferQueries(); - DataOfferContractOfferQueries dataOfferContractOfferQueries = new DataOfferContractOfferQueries(); + ContractOfferQueries contractOfferQueries = new ContractOfferQueries(); ContractOfferRecordUpdater contractOfferRecordUpdater = new ContractOfferRecordUpdater(); DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater(); DataOfferPatchBuilder dataOfferPatchBuilder = new DataOfferPatchBuilder( - dataOfferContractOfferQueries, + contractOfferQueries, dataOfferQueries, dataOfferRecordUpdater, contractOfferRecordUpdater diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java index db42a2e3c..1b2f04c21 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java @@ -15,7 +15,7 @@ package de.sovity.edc.ext.brokerserver.services.refreshing.offers; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferContractOfferRecord; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -28,13 +28,13 @@ class DataOfferWriterTestResultHelper { private final @NotNull Map dataOffers; - private final @NotNull Map> contractOffers; + private final @NotNull Map> contractOffers; DataOfferWriterTestResultHelper(DSLContext dsl) { this.dataOffers = dsl.selectFrom(Tables.DATA_OFFER).fetchMap(Tables.DATA_OFFER.ASSET_ID); - this.contractOffers = dsl.selectFrom(Tables.DATA_OFFER_CONTRACT_OFFER).stream().collect(groupingBy( - DataOfferContractOfferRecord::getAssetId, - Collectors.toMap(DataOfferContractOfferRecord::getContractOfferId, Function.identity()) + this.contractOffers = dsl.selectFrom(Tables.CONTRACT_OFFER).stream().collect(groupingBy( + ContractOfferRecord::getAssetId, + Collectors.toMap(ContractOfferRecord::getContractOfferId, Function.identity()) )); } @@ -50,7 +50,7 @@ public int numContractOffers(String assetId) { return contractOffers.getOrDefault(assetId, Map.of()).size(); } - public DataOfferContractOfferRecord getContractOffer(String assetId, String contractOfferId) { + public ContractOfferRecord getContractOffer(String assetId, String contractOfferId) { return contractOffers.getOrDefault(assetId, Map.of()).get(contractOfferId); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java index 25f51fd7a..feb69efc4 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.brokerserver.services.schedules; -import de.sovity.edc.ext.brokerserver.TestUtils; import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; import de.sovity.edc.ext.brokerserver.db.TestDatabase; @@ -78,7 +77,7 @@ void test_offlineConnectorKiller_should_be_dead() { // assert dsl.select().from(CONNECTOR).fetch().forEach(record -> { - assertThat(record.get(CONNECTOR.CONNECTOR_ID)).isEqualTo("http://example.org"); + assertThat(record.get(CONNECTOR.ENDPOINT)).isEqualTo("https://my-connector/api/dsp"); assertThat(record.get(CONNECTOR.ONLINE_STATUS)).isEqualTo(ConnectorOnlineStatus.DEAD); }); }); @@ -96,7 +95,7 @@ void test_offlineConnectorKiller_should_not_be_dead() { // assert dsl.select().from(CONNECTOR).fetch().forEach(record -> { - assertThat(record.get(CONNECTOR.CONNECTOR_ID)).isEqualTo("http://example.org"); + assertThat(record.get(CONNECTOR.ENDPOINT)).isEqualTo("https://my-connector/api/dsp"); assertThat(record.get(CONNECTOR.ONLINE_STATUS)).isNotEqualTo(ConnectorOnlineStatus.DEAD); }); }); @@ -104,8 +103,8 @@ void test_offlineConnectorKiller_should_not_be_dead() { private static void createConnector(DSLContext dsl, int createdDaysAgo) { dsl.insertInto(CONNECTOR) - .set(CONNECTOR.CONNECTOR_ID, "http://example.org") - .set(CONNECTOR.ENDPOINT, TestUtils.MANAGEMENT_ENDPOINT) + .set(CONNECTOR.ENDPOINT, "https://my-connector/api/dsp") + .set(CONNECTOR.PARTICIPANT_ID, "my-connector") .set(CONNECTOR.ONLINE_STATUS, ConnectorOnlineStatus.OFFLINE) .set(CONNECTOR.LAST_SUCCESSFUL_REFRESH_AT, OffsetDateTime.now().minusDays(createdDaysAgo)) .set(CONNECTOR.CREATED_AT, OffsetDateTime.now().minusDays(6)) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java new file mode 100644 index 000000000..1279a31be --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JsonUtils2Test { + @Test + void equalityTests() { + assertTrue(JsonUtils2.isEqualJson(null, null)); + assertTrue(JsonUtils2.isEqualJson("null", "null")); + assertTrue(JsonUtils2.isEqualJson("{}", "{}")); + assertTrue(JsonUtils2.isEqualJson("{\"a\": true, \"b\": \"hello\"}", "{\"a\": true,\"b\": \"hello\"}")); + assertTrue(JsonUtils2.isEqualJson("{\"a\": true, \"b\": \"hello\"}", "{\"b\": \"hello\", \"a\": true}")); + + assertFalse(JsonUtils2.isEqualJson(null, "1")); + assertFalse(JsonUtils2.isEqualJson("1", null)); + } +} diff --git a/gradle.properties b/gradle.properties index 833a40846..69a547ff1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,13 +3,13 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=4.2.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc # Eclipse EDC edcGroup=org.eclipse.edc -edcVersion=0.0.1-20230220.patch1 +edcVersion=0.2.1 # Other Dependencies assertj=3.23.1 From daa9d3300a2d2ae6a6987827133b1a19930d1fe7 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 17 Nov 2023 09:27:18 +0100 Subject: [PATCH 156/295] fix: improve db connection usage in tests (#309) --- .../edc/ext/brokerserver/db/DataSourceFactory.java | 4 ++-- .../ext/brokerserver/db/PostgresFlywayExtension.java | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java index 7e2435ca0..70abc53e8 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java @@ -32,7 +32,7 @@ public class DataSourceFactory { * * @return {@link DataSource}. */ - public DataSource newDataSource() { + public HikariDataSource newDataSource() { var jdbcCredentials = JdbcCredentials.fromConfig(config); int maxPoolSize = config.getInteger(PostgresFlywayExtension.DB_CONNECTION_POOL_SIZE); int connectionTimeoutInMs = config.getInteger(PostgresFlywayExtension.DB_CONNECTION_TIMEOUT_IN_MS); @@ -49,7 +49,7 @@ public DataSource newDataSource() { * @param connectionTimeoutInMs connection timeout in ms * @return {@link DataSource}. */ - public static DataSource newDataSource( + public static HikariDataSource newDataSource( JdbcCredentials jdbcCredentials, int maxPoolSize, int connectionTimeoutInMs diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java index 0dcf2058a..c887c9a1b 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.db; +import com.zaxxer.hikari.HikariDataSource; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; @@ -36,6 +37,8 @@ public class PostgresFlywayExtension implements ServiceExtension { @Setting public static final String DB_CONNECTION_TIMEOUT_IN_MS = "edc.broker.server.db.connection.timeout.in.ms"; + private HikariDataSource dataSource; + @Override public String name() { return "Postgres Flyway Extension (Broker Server)"; @@ -47,11 +50,16 @@ public void initialize(ServiceExtensionContext context) { var monitor = context.getMonitor(); var dataSourceFactory = new DataSourceFactory(config); - var dataSource = dataSourceFactory.newDataSource(); + dataSource = dataSourceFactory.newDataSource(); var flywayFactory = new FlywayFactory(config); var flyway = flywayFactory.setupFlyway(dataSource); var flywayMigrator = new FlywayMigrator(flyway, config, monitor); flywayMigrator.migrateAndRepair(); } + + @Override + public void shutdown() { + dataSource.close(); + } } From dda7763e2eec63f0ee4484ee52b127dfaa327ea1 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 17 Nov 2023 10:58:22 +0100 Subject: [PATCH 157/295] chore: update deployment guide for EDC 0 (#310) --- .editorconfig | 4 + .env | 6 +- .github/ISSUE_TEMPLATE/release.md | 3 +- CHANGELOG.md | 12 ++- README.md | 76 ++++++++++++++++--- connector/.env | 47 +----------- connector/Dockerfile | 2 +- connector/build.gradle.kts | 1 + docker-compose.yaml | 71 +++++++++++------ .../client/BrokerServerClientFactory.java | 2 +- 10 files changed, 135 insertions(+), 89 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1da5f17fe..0f78b5fda 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,7 @@ quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false + +[docker-compose.yaml] +indent_size = 2 + diff --git a/.env b/.env index fb30adf4a..2c86bb67e 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:1.2.0 -EDC_CE_IMAGE=ghcr.io/sovity/edc-dev:4.2.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest +EDC_IMAGE=ghcr.io/sovity/edc-dev:5.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.0.0 diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 19ead817d..6b6997eb1 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -43,4 +43,5 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the Broker Server. - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the edc-extensions version `0.0.1-SNAPSHOT`. - [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-broker-server-extension/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. - - [ ] Merge the `release-cleanup` PR. \ No newline at end of file + - [ ] Merge the `release-cleanup` PR. +- [ ] Close this issue. diff --git a/CHANGELOG.md b/CHANGELOG.md index 28b88eec9..3aa154f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Patch +- Fixed some issues with DB Connections not released between tests. + ### Deployment Migration Notes -- All connectors need to be re-crawled for detailed asset metadata and participant IDs to work +1. Connectors and Data Offers require an initial crawl before their metadata is filled again. +2. Deployment Migration Notes for the Broker UI: https://github.com/sovity/edc-ui/releases/tag/v2.0.0 +3. The Protocol Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/ids`~~. +4. The Management Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/management`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/management`~~. +5. The Connector Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/ids/data`~~. ## [v1.2.0] - 2023-10-30 @@ -93,7 +99,7 @@ Bugfix release for the asset properties issue. Also contains the connector delet curl --request DELETE \ --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ - --header 'X-Api-Key: ApiKeyDefaultValue' \ + --header 'x-api-key: ApiKeyDefaultValue' \ --data '["https://some-connector-to-delete/api/dsp", "https://some-other-connector-to-delete/api/dsp"]' ``` @@ -209,7 +215,7 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde curl --request PUT \ --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ - --header 'X-Api-Key: ApiKeyDefaultValue' \ + --header 'x-api-key: ApiKeyDefaultValue' \ --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' ``` diff --git a/README.md b/README.md index e52b7f175..a15894ec8 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,11 @@ This IDS Broker is written on basis of the EDC and should be used in tandem with ## Development -For development, access to the GitHub Maven Registry is required. +### Local Development + +#### Local Backend Development + +For local backend development, access to the GitHub Maven Registry is required. To access the GitHub Maven Registry you need to provide the following properties, e.g. by providing a `~/.gradle/gradle.properties`. @@ -60,11 +64,59 @@ gpr.user={your github username} gpr.key={your github pat with packages.read} ``` +Developing the Broker Backend tests are used to validate functionality: + +- There are Integration Tests using the Broker Server Java Client Library for testing API Endpoints of a running + backend. +- There are Integration Tests using the Broker Server Java Client Library and sovity EDC Extensions to integration + test the Broker with a running EDC where communication works through the Data Space Protocol (DSP). +- There are Unit Tests with Mockito for testing local complexity, e.g. mappers, data structures, utilities. + +

(back to top)

+ +#### Local UI Development + +The Broker UI is a profile `broker` of the [EDC UI](https://github.com/sovity/edc-ui): + +The Broker UI depends on the NPM +Package [@sovity/broker-server-client](https://www.npmjs.com/package/@sovity.de/broker-server-client) built on the main +branch or on releases. + +Local Broker UI Development can start with the type-safe broker server fake backend once the Client Library version is +bumped to contain the up-to-date API Models. + +

(back to top)

+ +### Local E2E Development + +There is currently no support for Local E2E Development (a locally running backend build server and a locally running +frontend build server). + +For debugging UI issues, however, the UI can be manually configured to use a live backend, e.g. one started via +the [docker-compose.yaml](#local-demo). + +

(back to top)

+ +### Local Demo + +There is a [docker-compose.yaml](docker-compose.yaml) that starts a broker and a connector. + +At release time it is pinned down to the release versions. + +Mid-development it might be un-pinned back to latest versions. + +| | Broker | Conncetor | +|---------------------|------------------------------------------------------------------|:-----------------------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | +| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://broker:11003/api/dsp
Requires Docker Compose Network | http://connector:22003/api/dsp
Requires Docker Compose Network | +

(back to top)

## Releasing -Create an issue using the [release template](.github/ISSUE_TEMPLATE/release.md) and follow the instructions. +[Create a Release Issue](https://github.com/sovity/edc-broker-server-extension/issues/new?assignees=&labels=task%2Frelease%2Cscope%2Fmds&projects=&template=release.md&title=Release+x.x.x) and follow the instructions.

(back to top)

@@ -89,13 +141,13 @@ or if it's broken. - The broker is meant to be served via TLS/HTTPS. - The broker is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `80` port. + - The UI's `8080` port. - The Backend's `11002` port. - The Backend's `11003` port. - The mapping should look like this: - - `/backend/api/v1/ids` -> `broker-backend:11003/backend/api/v1/ids` - - `/backend/api/v1/management` -> `broker-backend:11002/backend/api/v1/management` - - All other requests should be mapped to `broker-ui:80` + - `https://[MY_EDC_FQDN]/backend/api/dsp` -> `broker-backend:11003/backend/api/dsp` + - `https://[MY_EDC_FQDN]/backend/api/management` -> `broker-backend:11002/backend/api/management` + - All other requests -> `broker-ui:8080` #### Backend Configuration @@ -146,10 +198,10 @@ in [connector/.env](connector/.env). EDC_UI_ACTIVE_PROFILE: broker # Required: Management API URL -EDC_UI_DATA_MANAGEMENT_API_URL: https://my-broker.com/backend/api/v1/management +EDC_UI_MANAGEMENT_API_URL: https://my-broker.com/backend/api/management # Required: Management API Key -EDC_API_AUTH_KEY: "ApiKeyDefaultValue" +EDC_UI_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" ``` #### Adding Connectors at runtime @@ -159,9 +211,9 @@ Connectors can be dynamically added at runtime by using the following endpoint: ```shell script # Response should be 204 No Content curl --request PUT \ - --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ - --header 'X-Api-Key: ApiKeyDefaultValue' \ + --header 'x-api-key: ApiKeyDefaultValue' \ --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' ``` @@ -172,9 +224,9 @@ Connectors can be dynamically removed at runtime by using the following endpoint ```shell script # Response should be 204 No Content curl --request DELETE \ - --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ --header 'Content-Type: application/json' \ - --header 'X-Api-Key: ApiKeyDefaultValue' \ + --header 'x-api-key: ApiKeyDefaultValue' \ --data '["https://some-connector-to-be-removed/api/dsp", "https://some-other-connector-to-be-removed/api/dsp"]' ``` diff --git a/connector/.env b/connector/.env index 587bc2e28..a5da42c83 100644 --- a/connector/.env +++ b/connector/.env @@ -79,22 +79,13 @@ WEB_HTTP_MANAGEMENT_PORT=11002 WEB_HTTP_PROTOCOL_PORT=11003 WEB_HTTP_CONTROL_PORT=11004 WEB_HTTP_PATH=${MY_EDC_BASE_PATH}/api -WEB_HTTP_MANAGEMENT_PATH=${MY_EDC_BASE_PATH}/api/v1/management -WEB_HTTP_PROTOCOL_PATH=${MY_EDC_BASE_PATH}/api/v1/ids -WEB_HTTP_CONTROL_PATH=${MY_EDC_BASE_PATH}/api/v1/control +WEB_HTTP_MANAGEMENT_PATH=${MY_EDC_BASE_PATH}/api/management +WEB_HTTP_PROTOCOL_PATH=${MY_EDC_BASE_PATH}/api/dsp +WEB_HTTP_CONTROL_PATH=${MY_EDC_BASE_PATH}/api/control EDC_CONNECTOR_NAME=$MY_EDC_NAME_KEBAB_CASE EDC_HOSTNAME=${MY_EDC_FQDN} - -# Deprecated IDS Settings -EDC_IDS_ID=urn:connector:$MY_EDC_NAME_KEBAB_CASE -EDC_IDS_TITLE=This will be unavailable starting Core EDC 0.1.0 -EDC_IDS_DESCRIPTION=This will be unavailable starting Core EDC 0.1.0 -EDC_IDS_CURATOR=http://this-will-be-unavailable-starting-core-edc-0-1-0 -EDC_IDS_MAINTAINER=http://this-will-be-unavailable-starting-core-edc-0-1-0 -MY_EDC_IDS_BASE_URL=${MY_EDC_PROTOCOL}${MY_EDC_FQDN} -IDS_WEBHOOK_ADDRESS=${MY_EDC_IDS_BASE_URL} -EDC_IDS_ENDPOINT=${MY_EDC_IDS_BASE_URL}${WEB_HTTP_PROTOCOL_PATH} +EDC_DSP_CALLBACK_ADDRESS=${MY_EDC_PROTOCOL}${MY_EDC_FQDN}${WEB_HTTP_PROTOCOL_PATH} # Flyway Extension: Defaults EDC_DATASOURCE_DEFAULT_NAME=default @@ -102,36 +93,6 @@ EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD -EDC_DATASOURCE_ASSET_NAME=asset -EDC_DATASOURCE_ASSET_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_ASSET_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_ASSET_PASSWORD=$MY_EDC_JDBC_PASSWORD - -EDC_DATASOURCE_CONTRACTDEFINITION_NAME=contractdefinition -EDC_DATASOURCE_CONTRACTDEFINITION_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_CONTRACTDEFINITION_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_CONTRACTDEFINITION_PASSWORD=$MY_EDC_JDBC_PASSWORD - -EDC_DATASOURCE_CONTRACTNEGOTIATION_NAME=contractnegotiation -EDC_DATASOURCE_CONTRACTNEGOTIATION_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_CONTRACTNEGOTIATION_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_CONTRACTNEGOTIATION_PASSWORD=$MY_EDC_JDBC_PASSWORD - -EDC_DATASOURCE_POLICY_NAME=policy -EDC_DATASOURCE_POLICY_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_POLICY_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_POLICY_PASSWORD=$MY_EDC_JDBC_PASSWORD - -EDC_DATASOURCE_TRANSFERPROCESS_NAME=transferprocess -EDC_DATASOURCE_TRANSFERPROCESS_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_TRANSFERPROCESS_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_TRANSFERPROCESS_PASSWORD=$MY_EDC_JDBC_PASSWORD - -EDC_DATASOURCE_DATAPLANEINSTANCE_NAME=dataplaneinstance -EDC_DATASOURCE_DATAPLANEINSTANCE_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_DATAPLANEINSTANCE_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_DATAPLANEINSTANCE_PASSWORD=$MY_EDC_JDBC_PASSWORD - # Oauth default configurations EDC_OAUTH_PROVIDER_AUDIENCE=idsc:IDS_CONNECTORS_ALL diff --git a/connector/Dockerfile b/connector/Dockerfile index 822159b04..30f8f33d0 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -34,7 +34,7 @@ COPY --from=build /home/gradle/project/connector/build/libs/app.jar /app COPY ./connector/src/main/resources/logging.properties /app # health status is determined by the availability of the /health endpoint -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "X-Api-Key: $EDC_API_AUTH_KEY" --fail http://localhost:11002/backend/api/v1/management/check/health +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11002/backend/api/v1/management/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. # ARG can not be used in ENTRYPOINT so storing values in ENV variables diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts index 40f54740a..81e0dd58e 100644 --- a/connector/build.gradle.kts +++ b/connector/build.gradle.kts @@ -10,6 +10,7 @@ val edcGroup: String by project dependencies { // Control-Plane implementation("${edcGroup}:control-plane-core:${edcVersion}") + implementation("${edcGroup}:data-plane-selector-core:${edcVersion}") implementation("${edcGroup}:api-observability:${edcVersion}") implementation("${edcGroup}:configuration-filesystem:${edcVersion}") implementation("${edcGroup}:control-plane-aggregate-services:${edcVersion}") diff --git a/docker-compose.yaml b/docker-compose.yaml index 39c2bc8f8..3196eaa28 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,33 +3,37 @@ services: broker-ui: image: ${EDC_UI_IMAGE} ports: - - '11000:80' + - '11000:8080' environment: - - EDC_UI_ACTIVE_PROFILE=broker - - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:11002/backend/api/v1/management - - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue + EDC_UI_ACTIVE_PROFILE: broker + EDC_UI_MANAGEMENT_API_URL: http://localhost:11002/backend/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + NGINX_ACCESS_LOG: off broker: image: ${BROKER_IMAGE} depends_on: - broker-postgresql - connector environment: - # Broker Configuration + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/dsp" + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" + + # Hide offline data offers after 1 minute in dev + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" + MY_EDC_FQDN: "broker" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc MY_EDC_JDBC_USER: edc MY_EDC_JDBC_PASSWORD: edc - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/dsp" - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" - # Local Dev / Docker-Compose Config - MY_EDC_PROTOCOL: "http://" # We don't have TLS in the docker container - MY_EDC_IDS_BASE_URL: "http://broker:11003" # Add the port, because we have no reverse proxy erasing the ports here + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://broker:11003/backend/api/dsp EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" # Hide offline data offers after 1 minute in dev - EDC_API_AUTH_KEY: "ApiKeyDefaultValue" # Management API Key (Access to UI should be secured by other means, as this key is sent to the UI) ports: - '11001:11001' - '11002:11002' @@ -44,36 +48,42 @@ services: POSTGRESQL_PASSWORD: edc POSTGRESQL_DATABASE: edc ports: - - '54321:5432' + - '54321:5432' volumes: - 'broker-postgresql:/bitnami/postgresql' connector-ui: image: ${EDC_UI_IMAGE} ports: - - '22000:80' + - '22000:8080' environment: - - EDC_UI_ACTIVE_PROFILE=mds-open-source - - EDC_UI_CONFIG_URL=edc-ui-config - - EDC_UI_DATA_MANAGEMENT_API_URL=http://localhost:22002/api/v1/management - - EDC_UI_DATA_MANAGEMENT_API_KEY=ApiKeyDefaultValue + EDC_UI_ACTIVE_PROFILE: mds-open-source + EDC_UI_CONFIG_URL: edc-ui-config + EDC_UI_MANAGEMENT_API_URL: http://localhost:22002/api/v1/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + NGINX_ACCESS_LOG: off connector: - image: ${EDC_CE_IMAGE} + image: ${EDC_IMAGE} + depends_on: + - connector-postgresql environment: - MY_EDC_NAME_KEBAB_CASE: "example-connector" + MY_EDC_NAME_KEBAB_CASE: "my-connector" MY_EDC_TITLE: "EDC Connector" - MY_EDC_DESCRIPTION: "MDS Community Edition EDC Connector" + MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" MY_EDC_CURATOR_URL: "https://example.com" MY_EDC_CURATOR_NAME: "Example GmbH" MY_EDC_MAINTAINER_URL: "https://sovity.de" MY_EDC_MAINTAINER_NAME: "sovity GmbH" - # Data Management API Key + MY_EDC_FQDN: "connector" EDC_API_AUTH_KEY: ApiKeyDefaultValue - # Local Dev / Docker-Compose Config + MY_EDC_JDBC_URL: jdbc:postgresql://connector-postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) MY_EDC_PROTOCOL: "http://" - MY_EDC_FQDN: "connector" - MY_EDC_IDS_BASE_URL: "http://connector:11003" + EDC_DSP_CALLBACK_ADDRESS: http://connector:11003/api/dsp EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' @@ -83,6 +93,17 @@ services: - '22003:11003' - '22004:11004' - '22005:5005' + connector-postgresql: + image: docker.io/bitnami/postgresql:11 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + ports: + - '54322:5432' + volumes: + - 'connector-postgresql:/bitnami/postgresql' volumes: broker-postgresql: driver: local diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java index 57303717c..01bb388a4 100644 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java +++ b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java @@ -32,7 +32,7 @@ public static BrokerServerClient newClient(BrokerServerClientBuilder builder) { .setBasePath(builder.managementApiUrl()); if (StringUtils.isNotBlank(builder.managementApiKey())) { - apiClient.addDefaultHeader("X-Api-Key", builder.managementApiKey()); + apiClient.addDefaultHeader("x-api-key", builder.managementApiKey()); } return new BrokerServerClient( From 42da9e85570cf683afb46cc955e062f570051a75 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 17 Nov 2023 16:55:29 +0100 Subject: [PATCH 158/295] fix: default configuration for the daps (#311) --- connector/.env | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/connector/.env b/connector/.env index a5da42c83..b0a4633d2 100644 --- a/connector/.env +++ b/connector/.env @@ -93,8 +93,10 @@ EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD -# Oauth default configurations -EDC_OAUTH_PROVIDER_AUDIENCE=idsc:IDS_CONNECTORS_ALL +# Oauth default configurations for compatibility with sovity DAPS +EDC_OAUTH_PROVIDER_AUDIENCE: ${EDC_OAUTH_TOKEN_URL} +EDC_OAUTH_ENDPOINT_AUDIENCE: idsc:IDS_CONNECTORS_ALL +EDC_AGENT_IDENTITY_KEY: sub # This file could contain an entry replacing the EDC_KEYSTORE ENV var, # but for some reason it is required, and EDC won't start up if it isn't configured. From 46d7ec74112b879df9ea7c0acf901720e7945c0b Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 17 Nov 2023 17:51:36 +0100 Subject: [PATCH 159/295] fix: default sorting not being first sorting (#312) --- .env | 2 +- CHANGELOG.md | 1 + docker-compose.yaml | 4 ++-- .../dao/pages/catalog/CatalogQuerySortingService.java | 5 +++-- .../dao/pages/connector/ConnectorListQueryService.java | 5 +++-- .../brokerserver/services/api/CatalogApiService.java | 10 ++++++++-- .../services/api/ConnectorListApiService.java | 10 ++++++++-- 7 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.env b/.env index 2c86bb67e..45ec352bb 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest EDC_IMAGE=ghcr.io/sovity/edc-dev:5.0.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa154f2a..56b0fe972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Patch - Fixed some issues with DB Connections not released between tests. +- Fixed issue with initial sorting not being the first sorting. ### Deployment Migration Notes diff --git a/docker-compose.yaml b/docker-compose.yaml index 3196eaa28..54c521d5d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -58,7 +58,7 @@ services: environment: EDC_UI_ACTIVE_PROFILE: mds-open-source EDC_UI_CONFIG_URL: edc-ui-config - EDC_UI_MANAGEMENT_API_URL: http://localhost:22002/api/v1/management + EDC_UI_MANAGEMENT_API_URL: http://localhost:22002/api/management EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue NGINX_ACCESS_LOG: off connector: @@ -94,7 +94,7 @@ services: - '22004:11004' - '22005:5005' connector-postgresql: - image: docker.io/bitnami/postgresql:11 + image: docker.io/bitnami/postgresql:15 restart: always environment: POSTGRESQL_USERNAME: edc diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java index e08fdfe46..c06aaecb1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jooq.OrderField; @@ -24,9 +25,9 @@ @RequiredArgsConstructor public class CatalogQuerySortingService { @NotNull - public List> getOrderBy(CatalogQueryFields fields, CatalogPageSortingType sorting) { + public List> getOrderBy(CatalogQueryFields fields, @NonNull CatalogPageSortingType sorting) { List> orderBy; - if (sorting == null || sorting == CatalogPageSortingType.TITLE) { + if (sorting == CatalogPageSortingType.TITLE) { orderBy = List.of( fields.getDataOfferTable().ASSET_TITLE.asc(), fields.getConnectorTable().ENDPOINT.asc() diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java index 18f860f71..c515c32b3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; +import lombok.NonNull; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.jooq.Field; @@ -49,7 +50,7 @@ public List queryConnectorPage(DSLContext dsl, String sear } @NotNull - private List> sortConnectorPage(Connector c, ConnectorPageSortingType sorting) { + private List> sortConnectorPage(Connector c, @NonNull ConnectorPageSortingType sorting) { var alphabetically = c.ENDPOINT.asc(); var recentFirst = c.CREATED_AT.desc(); var onlineStatus = DSL.case_(c.ONLINE_STATUS) @@ -58,7 +59,7 @@ private List> sortConnectorPage(Connector c, ConnectorPageSortingT .else_(3) .asc(); - if (sorting == null || sorting == ConnectorPageSortingType.ONLINE_STATUS) { + if (sorting == ConnectorPageSortingType.ONLINE_STATUS) { return List.of(onlineStatus, alphabetically); } else if (sorting == ConnectorPageSortingType.TITLE) { return List.of(alphabetically, recentFirst); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index 1ba483a06..0c694c779 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -52,12 +52,18 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { brokerServerSettings.getCatalogPagePageSize() ); + var availableSortings = buildAvailableSortings(); + var sorting = query.getSorting(); + if (sorting == null) { + sorting = availableSortings.get(0).getSorting(); + } + // execute db query var catalogPageRs = catalogQueryService.queryCatalogPage( dsl, query.getSearchQuery(), filters, - query.getSorting(), + sorting, pageQuery ); @@ -69,7 +75,7 @@ public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { ); var result = new CatalogPageResult(); - result.setAvailableSortings(buildAvailableSortings()); + result.setAvailableSortings(availableSortings); result.setPaginationMetadata(paginationMetadata); result.setAvailableFilters(catalogFilterService.buildAvailableFilters(catalogPageRs.getAvailableFilterValues())); result.setDataOffers(buildCatalogDataOffers(catalogPageRs.getDataOffers())); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java index fe41c641a..6c1af7645 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java @@ -37,10 +37,16 @@ public class ConnectorListApiService { public ConnectorPageResult connectorListPage(DSLContext dsl, ConnectorPageQuery query) { Objects.requireNonNull(query, "query must not be null"); - var connectorDbRows = connectorListQueryService.queryConnectorPage(dsl, query.getSearchQuery(), query.getSorting()); + var availableSortings = buildAvailableSortings(); + var sorting = query.getSorting(); + if (sorting == null) { + sorting = availableSortings.get(0).getSorting(); + } + + var connectorDbRows = connectorListQueryService.queryConnectorPage(dsl, query.getSearchQuery(), sorting); var result = new ConnectorPageResult(); - result.setAvailableSortings(buildAvailableSortings()); + result.setAvailableSortings(availableSortings); result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); result.setConnectors(buildConnectorListEntries(connectorDbRows)); return result; From 62967b8e012fa132ed5c21580966c8324a356b8c Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 17 Nov 2023 20:49:12 +0100 Subject: [PATCH 160/295] chore: prepare release (#314) --- .env | 6 +++--- CHANGELOG.md | 30 +++++++++++++++++++++++++++++- gradle.properties | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 45ec352bb..0fe62d44d 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest -EDC_IMAGE=ghcr.io/sovity/edc-dev:5.0.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:2.0.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:6.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b0fe972..8f9cffc5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major +#### Minor + +#### Patch + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION }}` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` +- Sovity EDC CE: {{ CE Release Link }} + +## [v2.0.0] - 2023-11-17 + +### Overview + +EDC 0 Release, some bugfixes. + +### Detailed Changes + +#### Major + - Migrated to Eclipse EDC 0.2.1 - Migrated to edc-extensions 5.0.0 - Migrated Assets to JSON-LD @@ -30,11 +52,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deployment Migration Notes 1. Connectors and Data Offers require an initial crawl before their metadata is filled again. -2. Deployment Migration Notes for the Broker UI: https://github.com/sovity/edc-ui/releases/tag/v2.0.0 +2. UI Migration Notes since the last Broker Release: https://github.com/sovity/edc-ui/releases/tag/v2.0.0 3. The Protocol Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/ids`~~. 4. The Management Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/management`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/management`~~. 5. The Connector Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/ids/data`~~. +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` +- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/6.0.0) + ## [v1.2.0] - 2023-10-30 ### Overview diff --git a/gradle.properties b/gradle.properties index 69a547ff1..2262570d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=6.0.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From e7acfa4c417cd62a4b300e241442769da4ab458b Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 20 Nov 2023 11:41:27 +0100 Subject: [PATCH 161/295] fix: DAPS configuration in .env --- connector/.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connector/.env b/connector/.env index b0a4633d2..f47574cf3 100644 --- a/connector/.env +++ b/connector/.env @@ -94,9 +94,9 @@ EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD # Oauth default configurations for compatibility with sovity DAPS -EDC_OAUTH_PROVIDER_AUDIENCE: ${EDC_OAUTH_TOKEN_URL} -EDC_OAUTH_ENDPOINT_AUDIENCE: idsc:IDS_CONNECTORS_ALL -EDC_AGENT_IDENTITY_KEY: sub +EDC_OAUTH_PROVIDER_AUDIENCE=${EDC_OAUTH_TOKEN_URL} +EDC_OAUTH_ENDPOINT_AUDIENCE=idsc:IDS_CONNECTORS_ALL +EDC_AGENT_IDENTITY_KEY=sub # This file could contain an entry replacing the EDC_KEYSTORE ENV var, # but for some reason it is required, and EDC won't start up if it isn't configured. From 9320028ddb8247c6008986dc7580cbfd9f659971 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 20 Nov 2023 12:36:02 +0100 Subject: [PATCH 162/295] chore: prepare bugfix release (#316) --- .env | 2 +- CHANGELOG.md | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 0fe62d44d..9bcffb4aa 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:2.0.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:2.0.1 EDC_IMAGE=ghcr.io/sovity/edc-dev:6.0.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9cffc5e..7033fd17d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v2.0.1] - 2023-11-17 + +### Overview + +EDC 0 Bugfix Release. + +### Detailed Changes + +#### Patch + +- Fixed an issue preventing DAPS roll-in with the `broker-server-ce` variant. + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.1` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` +- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extengsions/releases/tag/v6.0.0) + + ## [v2.0.0] - 2023-11-17 ### Overview @@ -61,7 +82,7 @@ EDC 0 Release, some bugfixes. - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.0` - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` -- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/6.0.0) +- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) ## [v1.2.0] - 2023-10-30 From bf1f6b71ba0f7db86e0bed93b055d82bde9594fc Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 21 Nov 2023 09:18:04 +0100 Subject: [PATCH 163/295] chore: update gh templates (#317) --- .github/ISSUE_TEMPLATE/enhancement.md | 27 +++++++++ .github/ISSUE_TEMPLATE/epic_template.md | 60 ------------------- ...ture_request.md => mds_feature_request.md} | 14 ++--- .github/ISSUE_TEMPLATE/release.md | 9 ++- .github/PULL_REQUEST_TEMPLATE.md | 47 +++------------ docs/dev/changelog_updates.md | 60 +++++++++++++++++++ 6 files changed, 108 insertions(+), 109 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/enhancement.md delete mode 100644 .github/ISSUE_TEMPLATE/epic_template.md rename .github/ISSUE_TEMPLATE/{feature_request.md => mds_feature_request.md} (77%) create mode 100644 docs/dev/changelog_updates.md diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 000000000..758935ff3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,27 @@ +--- +name: Enhancement +about: Implement a task / improve something +title: "" +labels: ["kind/enhancement", "scope/mds"] +assignees: "" +--- + +# Enhancement + +## Description + +_A clear and concise description of what the customer wants to happen._ + +- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. + +## Stakeholders + +_Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ + +## Solution Proposal and Work Breakdown + +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine a Solution Proposal / Work Breakdown +``` + diff --git a/.github/ISSUE_TEMPLATE/epic_template.md b/.github/ISSUE_TEMPLATE/epic_template.md deleted file mode 100644 index 961c29ada..000000000 --- a/.github/ISSUE_TEMPLATE/epic_template.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: Epic -about: Help us with new ideas -title: "" -labels: ["kind/epic", "scope/mds"] -assignees: "" ---- - -# Epic - -## Description - -_Brief summary of what this Epic is, whether it's a larger project, goal, or user story. Describe the job to be done, which persona this Epic is mainly for, or if more multiple, break it down by user and job story._ - -### Requirements - -_Which requirements do you have to be fulfilled?_ - -- Requirement 1 -- Requirement 2 - -## Work Breakdown - -_Create Stories which can be converted into issues_ - -- [ ] Story 1 -- [ ] Story 2 -- ... - -## Initiative / goal - -_Describe how this Epic impacts an initiative the business is working on._ - -### Hypothesis - -_What is your hypothesis on the success of this Epic? Describe how success will be measured and what leading indicators the team will have to know if success has been hit._ - -## Acceptance criteria and must have scope - -_Define what is a must-have for launch and in-scope. Keep this section fluid and dynamic until you lock-in priority during planning._ - -- Criteria 1 -- Criteria 2 -- ... - -## Stakeholders - -_Describe who needs to be kept up-to-date about this Epic, included in discussions, or updated along the way. Stakeholders can be both in Product/Engineering, as well as other teams like Customer Success who might want to keep customers updated on the epic project._ - -## Timeline - -_What's the timeline for this Epic, what resources are needed, and what might potentially block this from hitting the projected end date._ - -## Need for refinement - -_Which questions are open? From whom do you need more input to fully specify the epic?_ - -- Question 1 -- Question 2 -- ... diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/mds_feature_request.md similarity index 77% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to .github/ISSUE_TEMPLATE/mds_feature_request.md index 4afec849d..4b55f531a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/mds_feature_request.md @@ -1,9 +1,9 @@ --- -name: Feature Request +name: MDS Feature Request about: Help us improve your product experience with new features suggestions title: "" -labels: ["kind/enhancement", "scope/mds"] -assignees: "" +labels: ["kind/enhancement", "scope/mds", "status/blocked/needs-product"] +assignees: ["jkbquabeck", "AbdullahMuk"] --- # Feature Request @@ -32,8 +32,8 @@ _Add more on who asked for this, i.e. company, person, how much they pay us, wha ## (For sovity Team to complete) Solution Proposal and Work Breakdown -_If possible, provide a (brief!) solution proposal._ +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine further action items for this feature request +``` -- [ ] Step 1 -- [ ] Step 2 -- ... diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 6b6997eb1..fbd3a9da2 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -36,12 +36,15 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). - [ ] Checkout the release tag and check test the `docker-compose.yaml`. - [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. -- [ ] Notify the deployment team with Deployment Docs Zip file attached to the release, which should now contain both product changes and a deployment migration guide. -- [ ] `release-cleanup` PR: +- [ ] Send out a release notification E-Mail to the MDS, the MDS integrator company and the MDS operator company. + - [ ] Check @jkbquabeck for an up-to-date mailing list, separated into "To" and "Cc". + - [ ] Attach the Deployment Docs Zip generated during the GitHub release, which should now contain the CHANGELOG, deployment migration notes, an initial deployment guide and a local demo docker compose. +- [ ] Optional, this can be done mid-development if required: + - [ ] Create a `release-cleanup` PR. - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the EDC UI. - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the EDC CE. - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the Broker Server. - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the edc-extensions version `0.0.1-SNAPSHOT`. - - [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-broker-server-extension/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. - [ ] Merge the `release-cleanup` PR. +- [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-broker-server-extension/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. - [ ] Close this issue. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb93cb530..d2c9b82d6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,43 +1,12 @@ -# Pull Request -_Briefly describe WHAT your PR changes, which features it adds/modifies._ +_What issues does this PR close?_ -## How Has This Been Tested? -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration +```[tasklist] +### Checklist +- [ ] The PR title is short and expressive. +- [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/edc-broker-server-extension/tree/main/docs/dev/changelog_updates.md) for more information. +- [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. +- [ ] I have performed a **self-review** +``` -- Test A -- Test B -- ... - -**Test Configuration**: - -- Firmware version: -- Hardware: -- Toolchain: -- SDK: - -## Linked Issue(s) - -_Use keywords to automate: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword_ - -- fixes # (issue) -- closes # (issue) -- ... - -## PR is blocked by - -- [ ] blocked by # (issue) - -# Checklist - -- [ ] I have **formatted the title** correctly and precisely -- [ ] My code follows the **style guidelines** of this project -- [ ] I have performed a **self-review** of my own code -- [ ] I have **commented** my code, particularly in hard-to-understand areas and public classes/methods -- [ ] I have made corresponding changes to the **documentation** -- [ ] My changes generate **no new warnings** (performed checkstyle check locally) -- [ ] I have added **tests that prove my fix** is effective or that my feature works -- [ ] New and existing unit **tests pass locally** with my changes -- [ ] Any dependent **changes have been merged** and published in downstream modules -- [ ] I have added/updated **copyright headers** diff --git a/docs/dev/changelog_updates.md b/docs/dev/changelog_updates.md new file mode 100644 index 000000000..d4e7fcfe7 --- /dev/null +++ b/docs/dev/changelog_updates.md @@ -0,0 +1,60 @@ +Updating the Changelog +====================== + +This project uses a [CHANGELOG.md](../../CHANGELOG.md). + +## Structure of the Changelog + +Each pull request should also update the "Unreleased" section of the changelog. +It should also update the "Deployment Migration Notes" Section of the unreleased section as preparation for the release. + +For each release there will be a separate section especially with an "Overview" section containing a summary +from a product perspective. + +Releases will especially contain a "Compatible Versions" section with the final docker +images and versions of other software components that are connected by APIs. + +## How to categorize a change + +The changelog uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Changes are categorized as either Major, Minor or Patch Changes. + +For this project, changes are categorized as the following: + +### Major Changes + +Major changes include: + +- UX / Product overhauls. +- Breaking Changes in Connector-To-Connector communication +- Breaking Changes to the required deployment units. +- Breaking Changes in APIs for third party applications. + +### Minor Changes + +Minor changes include: + +- New or changed features from a customer perspective. +- New APIs with API contracts with other deployment units (our UI doesn't count). +- New Product Documentation + +### Patch Changes + +Patch changes are basically everything else, that does not add, change or remove any product or external API features. + +- Product Fixes, Bugfixes, Refactorings +- Changes to existing Product Documentation +- New or changes to existing Developer Documentation +- Everything else + +## Released Versions + +On releases the "Unreleased" section is emptied in favor of a new section for the release. + +Whether a release will bump the major, minor or patch version is decided by the unreleased changes in the changelog. + +The Release sections will be cleaned up on release, improved with additional information and made +useful for the customer and people deploying the application, containing both product changes and +deployment migration notes. + +More on that can be found in the [Release Issue Template](../../.github/ISSUE_TEMPLATE/release.md). From e48d730375f3955d130800e0ee3739e6e4f70a63 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 21 Nov 2023 09:20:02 +0100 Subject: [PATCH 164/295] chore: fix bug report issue template --- .github/ISSUE_TEMPLATE/bug_report.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c61ca1a46..6fef046ca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -44,8 +44,7 @@ _Add any other context about the problem here._ ## Possible Implementation and Work Breakdown -_You already know the root cause of the erroneous state and how to fix it? Feel free to share your thoughts._ - -- [ ] Task 1 -- [ ] Task 2 -- ... +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine a Solution Proposal / Work Breakdown +``` From cffcd0c2b162e2ffc0ef265fc9ac4c563ea6ab22 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 23 Nov 2023 11:58:22 +0100 Subject: [PATCH 165/295] fix: healtcheck and prepare bugfix release (#326) --- .env | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++++++-- connector/Dockerfile | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 9bcffb4aa..fa25f9c03 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:2.0.1 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:2.0.2 EDC_IMAGE=ghcr.io/sovity/edc-dev:6.0.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7033fd17d..97b26a95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} -## [v2.0.1] - 2023-11-17 +## [v2.0.2] - 2023-11-23 + +### Overview + +EDC 0 Bugfix Release. + +### Detailed Changes + +#### Patch + +- Fixed an issue with the healthcheck. + +### Deployment Migration Notes + +_No special migration steps required._ + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.2` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` +- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) + +## [v2.0.1] - 2023-11-20 ### Overview @@ -39,11 +61,13 @@ EDC 0 Bugfix Release. ### Deployment Migration Notes +_No special migration steps required._ + #### Compatible Versions - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.1` - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` -- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extengsions/releases/tag/v6.0.0) +- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) ## [v2.0.0] - 2023-11-17 diff --git a/connector/Dockerfile b/connector/Dockerfile index 30f8f33d0..283f08853 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -34,7 +34,7 @@ COPY --from=build /home/gradle/project/connector/build/libs/app.jar /app COPY ./connector/src/main/resources/logging.properties /app # health status is determined by the availability of the /health endpoint -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11002/backend/api/v1/management/check/health +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11001/backend/api/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. # ARG can not be used in ENTRYPOINT so storing values in ENV variables From 7c1996fe71146fbb2d00570d2ab905eb9a2d0ef1 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:29:53 +0100 Subject: [PATCH 166/295] feat: Rework Data Offer count endpoint to Connector Metadata endpoint (#327) --- CHANGELOG.md | 6 ++ .../api/BrokerServerResource.java | 8 +- .../model/AuthorityPortalConnectorInfo.java | 29 +++++++ .../api/model/DataOfferCountResult.java | 21 ----- .../BrokerServerExtensionContextBuilder.java | 9 ++- .../BrokerServerResourceImpl.java | 11 +-- .../dao/pages/catalog/CatalogQueryFields.java | 12 ++- ...rityPortalConnectorMetadataApiService.java | 40 ++++++++++ .../AuthorityPortalConnectorQueryService.java | 75 ++++++++++++++++++ .../api/DataOfferCountApiService.java | 49 ------------ ...est.java => ConnectorMetadataApiTest.java} | 77 +++++++++++-------- 11 files changed, 220 insertions(+), 117 deletions(-) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java rename extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/{DataOfferCountApiTest.java => ConnectorMetadataApiTest.java} (64%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b26a95b..efb02a2dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major +- Authority Portal API: Removed data offer count endpoint in favor of new Connector Metadata Endpoint. + #### Minor +- Authority Portal API: Added new Connector Metadata endpoint that includes online status, participant ID and data offer counts. + #### Patch ### Deployment Migration Notes +- Authority Portal API: The data offer count endpoint was removed in favor of the new Connector Metadata Endpoint: `wrapper/broker/authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. + #### Compatible Versions - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION }}` diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index 92be0fca4..d87b4f68f 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -20,7 +20,7 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import io.swagger.v3.oas.annotations.Operation; @@ -81,9 +81,9 @@ public interface BrokerServerResource { void deleteConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); @POST - @Path("authority-portal-api/data-offer-counts") + @Path("authority-portal-api/connectors") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query the amount of public Data Offers by provided Connector URLs") - DataOfferCountResult dataOfferCount(List endpoints); + @Operation(description = "Provide Connector metadata by provided Connector Endpoints") + List getConnectorMetadata(List endpoints, @QueryParam("adminApiKey") String adminApiKey); } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java new file mode 100644 index 000000000..7a9061cbf --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java @@ -0,0 +1,29 @@ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Information for one connector, as required for the Authority Portal.", requiredMode = Schema.RequiredMode.REQUIRED) +public class AuthorityPortalConnectorInfo { + @Schema(description = "Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + @Schema(description = "Connector Participant ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String participantId; + @Schema(description = "Number of public Data Offers in this connector, as tracked by the broker", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer dataOfferCount; + @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorOnlineStatus onlineStatus; + @Schema(description = "Last successful refresh time stamp of the online status", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime offlineSinceOrLastUpdatedAt; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java deleted file mode 100644 index 061644ef3..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.Map; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Number of Data Offers per Connector endpoint.", requiredMode = Schema.RequiredMode.REQUIRED) -public class DataOfferCountResult { - @Schema(description = "Map from endpoint URL to Data Offer count", requiredMode = Schema.RequiredMode.REQUIRED) - private Map dataOfferCount; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 89b7477b9..3180d56a3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -38,13 +38,14 @@ import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorQueryService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorListApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorOnlineStatusMapper; import de.sovity.edc.ext.brokerserver.services.api.ConnectorService; -import de.sovity.edc.ext.brokerserver.services.api.DataOfferCountApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferMappingUtils; import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; @@ -282,7 +283,11 @@ public static BrokerServerExtensionContext buildContext( viewCountLogger, dataOfferMappingUtils ); - var dataOfferCountApiService = new DataOfferCountApiService(); + var connectorQueryService = new AuthorityPortalConnectorQueryService(); + var dataOfferCountApiService = new AuthorityPortalConnectorMetadataApiService( + connectorQueryService, + connectorOnlineStatusMapper + ); var connectorDetailApiService = new ConnectorDetailApiService(connectorDetailQueryService, connectorOnlineStatusMapper); var connectorListApiService = new ConnectorListApiService(connectorListQueryService, connectorOnlineStatusMapper, paginationMetadataUtils); var brokerServerResource = new BrokerServerResourceImpl( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 32fc227ff..0bdd80d23 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -15,21 +15,21 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorListApiService; -import de.sovity.edc.ext.brokerserver.services.api.DataOfferCountApiService; import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; import lombok.RequiredArgsConstructor; @@ -49,7 +49,7 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final CatalogApiService catalogApiService; private final DataOfferDetailApiService dataOfferDetailApiService; private final AdminApiKeyValidator adminApiKeyValidator; - private final DataOfferCountApiService dataOfferCountApiService; + private final AuthorityPortalConnectorMetadataApiService authorityPortalConnectorMetadataApiService; @Override public CatalogPageResult catalogPage(CatalogPageQuery query) { @@ -84,7 +84,8 @@ public void deleteConnectors(List endpoints, String adminApiKey) { } @Override - public DataOfferCountResult dataOfferCount(List endpoints) { - return dslContextFactory.transactionResult(dsl -> dataOfferCountApiService.countByEndpoints(dsl, endpoints)); + public List getConnectorMetadata(List endpoints, String adminApiKey) { + adminApiKeyValidator.validateAdminApiKey(adminApiKey); + return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.getMetadataByEndpoints(dsl, endpoints)); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index f9dc09795..da19faf16 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -58,10 +58,7 @@ public CatalogQueryFields( this.dataOfferTable = dataOfferTable; this.dataOfferViewCountTable = dataOfferViewCountTable; this.dataSpaceConfig = dataSpaceConfig; - offlineSinceOrLastUpdatedAt = DSL.coalesce( - connectorTable.LAST_SUCCESSFUL_REFRESH_AT, - connectorTable.CREATED_AT - ); + offlineSinceOrLastUpdatedAt = offlineSinceOrLastUpdatedAt(connectorTable); dataSpace = buildDataSpaceField(connectorTable, dataSpaceConfig); } @@ -105,4 +102,11 @@ public Field getViewCount() { return subquery.asField(); } + + public static Field offlineSinceOrLastUpdatedAt(Connector connectorTable) { + return DSL.coalesce( + connectorTable.LAST_SUCCESSFUL_REFRESH_AT, + connectorTable.CREATED_AT + ); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java new file mode 100644 index 000000000..60d103bef --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.List; + +@RequiredArgsConstructor +public class AuthorityPortalConnectorMetadataApiService { + private final AuthorityPortalConnectorQueryService authorityPortalConnectorQueryService; + private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; + + public List getMetadataByEndpoints(DSLContext dsl, List endpoints) { + + return authorityPortalConnectorQueryService.getConnectorMetadata(dsl, endpoints).stream() + .map(it -> new AuthorityPortalConnectorInfo( + it.getConnectorEndpoint(), + it.getParticipantId(), + it.getDataOfferCount(), + connectorOnlineStatusMapper.getOnlineStatus(it.getOnlineStatus()), + it.getOfflineSinceOrLastUpdatedAt() + )) + .toList(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java new file mode 100644 index 000000000..d6932baac --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import lombok.AccessLevel; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.time.OffsetDateTime; +import java.util.List; + +import static org.jooq.impl.DSL.coalesce; +import static org.jooq.impl.DSL.count; +import static org.jooq.impl.DSL.select; + +@RequiredArgsConstructor +public class AuthorityPortalConnectorQueryService { + + @Data + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class ConnectorMetadataRs { + String connectorEndpoint; + String participantId; + ConnectorOnlineStatus onlineStatus; + OffsetDateTime offlineSinceOrLastUpdatedAt; + Integer dataOfferCount; + } + + @NotNull + public List getConnectorMetadata(DSLContext dsl, List endpoints) { + var c = Tables.CONNECTOR; + + return dsl.select( + c.ENDPOINT.as("connectorEndpoint"), + c.PARTICIPANT_ID.as("participantId"), + c.ONLINE_STATUS.as("onlineStatus"), + CatalogQueryFields.offlineSinceOrLastUpdatedAt(c).as("offlineSinceOrLastUpdatedAt"), + getDataOfferCount(c.ENDPOINT).as("dataOfferCount") + ) + .from(c) + .where(PostgresqlUtils.in(c.ENDPOINT, endpoints)) + .fetchInto(ConnectorMetadataRs.class); + } + + @NotNull + public Field getDataOfferCount(Field connectorEndpoint) { + var d = Tables.DATA_OFFER; + + return select(coalesce(count().cast(Integer.class), DSL.value(0))) + .from(d) + .where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)) + .asField(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java deleted file mode 100644 index f43147bb1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiService.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; -import org.jooq.impl.DSL; - -import java.util.List; -import java.util.function.Function; - -import static java.util.stream.Collectors.toMap; - -@RequiredArgsConstructor -public class DataOfferCountApiService { - - public DataOfferCountResult countByEndpoints(DSLContext dsl, List endpoints) { - var d = Tables.DATA_OFFER; - - var count = DSL.count().as("count"); - var numDataOffers = dsl.select(d.CONNECTOR_ENDPOINT, count) - .from(d) - .where(PostgresqlUtils.in(d.CONNECTOR_ENDPOINT, endpoints)) - .groupBy(d.CONNECTOR_ENDPOINT) - .fetchMap(d.CONNECTOR_ENDPOINT, count); - - var numDataOffersFilled = endpoints.stream().distinct().collect(toMap( - Function.identity(), - endpoint -> numDataOffers.getOrDefault(endpoint, 0) - )); - - return new DataOfferCountResult(numDataOffersFilled); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorMetadataApiTest.java similarity index 64% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java rename to extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorMetadataApiTest.java index 722ee6747..26cd62871 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferCountApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorMetadataApiTest.java @@ -15,16 +15,15 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.TestPolicy; +import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.policy.model.Policy; import org.jooq.DSLContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,18 +32,19 @@ import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.List; import java.util.Map; import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; +import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static groovy.json.JsonOutput.toJson; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @ExtendWith(EdcExtension.class) -class DataOfferCountApiTest { +class ConnectorMetadataApiTest { @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); @@ -55,8 +55,9 @@ void setUp(EdcExtension extension) { } @Test - void testCountByEndpoints() { + void testConnectorMetadataByEndpoints() { TEST_DATABASE.testTransaction(dsl -> { + // arrange var now = OffsetDateTime.now().withNano(0); createConnector(dsl, now, 1); @@ -71,29 +72,51 @@ void testCountByEndpoints() { createConnector(dsl, now, 4); - var actual = brokerServerClient().brokerServerApi().dataOfferCount(Arrays.asList( + // act + var actual = brokerServerClient().brokerServerApi().getConnectorMetadata( + ADMIN_API_KEY, + Arrays.asList( getEndpoint(1), getEndpoint(1), // having this twice should not crash the query getEndpoint(2), - getEndpoint(4) - )); - var dataOfferCountMap = actual.getDataOfferCount(); - assertThat(dataOfferCountMap).isEqualTo(Map.of( - getEndpoint(1), 2, - getEndpoint(2), 1, - getEndpoint(4), 0 + getEndpoint(4), + getEndpoint(5) // having this not existing should not crash the query )); + + // assert + var first = forConnector(actual, 1); + assertThat(first.getParticipantId()).isEqualTo("my-connector"); + assertThat(first.getDataOfferCount()).isEqualTo(2); + assertThat(first.getOnlineStatus()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE); + assertThat(first.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); + var second = forConnector(actual, 2); + assertThat(second.getDataOfferCount()).isEqualTo(1); + assertThat(second.getOnlineStatus()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE); + assertThat(second.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); + var fourth = forConnector(actual, 4); + assertThat(fourth.getDataOfferCount()).isEqualTo(0); + assertThat(fourth.getOnlineStatus()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE); + assertThat(fourth.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); }); } - private void createConnector(DSLContext dsl, OffsetDateTime today, int iConnector) { + private AuthorityPortalConnectorInfo forConnector(List actual, int iConnector) { + return actual.stream() + .filter(connectorMetadata -> + getEndpoint(iConnector).equals(connectorMetadata.getConnectorEndpoint()) + ) + .findFirst() + .orElseThrow(); + } + + private void createConnector(DSLContext dsl, OffsetDateTime now, int iConnector) { var connector = dsl.newRecord(Tables.CONNECTOR); connector.setParticipantId("my-connector"); connector.setEndpoint(getEndpoint(iConnector)); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(today.minusDays(1)); - connector.setLastRefreshAttemptAt(today); - connector.setLastSuccessfulRefreshAt(today); + connector.setCreatedAt(now.minusDays(1)); + connector.setLastRefreshAttemptAt(now); + connector.setLastSuccessfulRefreshAt(now); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); connector.insert(); @@ -103,34 +126,24 @@ private String getEndpoint(int iConnector) { return "https://connector-%d".formatted(iConnector); } - private void createDataOffer(DSLContext dsl, OffsetDateTime today, int iConnector, int iDataOffer) { + private void createDataOffer(DSLContext dsl, OffsetDateTime now, int iConnector, int iDataOffer) { var connectorEndpoint = getEndpoint(iConnector); var assetJsonLd = getAssetJsonLd("my-asset-%d".formatted(iDataOffer)); var dataOffer = dsl.newRecord(Tables.DATA_OFFER); setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setCreatedAt(today.minusDays(5)); - dataOffer.setUpdatedAt(today); + dataOffer.setCreatedAt(now.minusDays(5)); + dataOffer.setUpdatedAt(now); dataOffer.insert(); var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); contractOffer.setContractOfferId("my-contract-offer-1"); contractOffer.setConnectorEndpoint(connectorEndpoint); contractOffer.setAssetId(dataOffer.getAssetId()); - contractOffer.setCreatedAt(today.minusDays(5)); - contractOffer.setUpdatedAt(today); + contractOffer.setCreatedAt(now.minusDays(5)); + contractOffer.setUpdatedAt(now); contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); contractOffer.insert(); } - - private Policy dummyPolicy() { - return Policy.Builder.newInstance() - .assignee("Example Assignee") - .build(); - } - - private String policyToJson(Policy policy) { - return toJson(policy); - } } From c99cf08030631c091a3be0a2b39bd9999cffbf47 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 6 Dec 2023 17:15:06 +0100 Subject: [PATCH 167/295] chore: prepare release (#332) --- .env | 6 ++-- .github/ISSUE_TEMPLATE/release.md | 3 +- CHANGELOG.md | 53 ++++++++++++++++++++++++------- connector/.env | 6 ++-- gradle.properties | 2 +- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/.env b/.env index fa25f9c03..7b73ad7d8 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:2.0.2 -EDC_IMAGE=ghcr.io/sovity/edc-dev:6.0.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.1.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.0.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.2.0 diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index fbd3a9da2..b56e6b6bc 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -1,7 +1,7 @@ --- name: Release about: Create an issue to track a release process. -title: "Release x.x.x" +title: "Release vx.x.x" labels: ["task/release", "scope/mds"] assignees: "" --- @@ -35,6 +35,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Create a release and re-use the changelog section as release description, and the version as title. - [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). - [ ] Checkout the release tag and check test the `docker-compose.yaml`. + - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. - [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. - [ ] Send out a release notification E-Mail to the MDS, the MDS integrator company and the MDS operator company. - [ ] Check @jkbquabeck for an up-to-date mailing list, separated into "To" and "Cc". diff --git a/CHANGELOG.md b/CHANGELOG.md index efb02a2dc..e5f1d1080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,24 +13,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major -- Authority Portal API: Removed data offer count endpoint in favor of new Connector Metadata Endpoint. - #### Minor -- Authority Portal API: Added new Connector Metadata endpoint that includes online status, participant ID and data offer counts. - #### Patch ### Deployment Migration Notes -- Authority Portal API: The data offer count endpoint was removed in favor of the new Connector Metadata Endpoint: `wrapper/broker/authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. - #### Compatible Versions - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION }}` - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v3.0.0] - 2023-06-12 + +### Overview + +EDC 0 / MDS 2.0 bugfix release, Authority Portal API Connector Metadata Endpoint. + +### Detailed Changes + +#### Major + +- Authority Portal API: Removed data offer count endpoint in favor of new Connector Metadata Endpoint. + +#### Minor + +- Bumped sovity EDC CE to `7.0.0`. +- Bumped Broker UI to `2.2.0`. +- Authority Portal API: Added new Connector Metadata endpoint that includes online status, participant ID and data offer + counts. + +### Deployment Migration Notes + +- The DAPS needs to contain the claim `referringConnector=broker` for the broker. The expected value `broker` could be overridden by + specifying a different value for `MY_EDC_PARTICIPANT_ID`. +- Authority Portal API: The data offer count endpoint was removed in favor of the new Connector Metadata + Endpoint: `wrapper/broker/authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.0.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.2.0` +- Sovity EDC CE: [`7.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.0.0) + ## [v2.0.2] - 2023-11-23 ### Overview @@ -75,7 +101,6 @@ _No special migration steps required._ - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` - Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) - ## [v2.0.0] - 2023-11-17 ### Overview @@ -104,9 +129,12 @@ EDC 0 Release, some bugfixes. 1. Connectors and Data Offers require an initial crawl before their metadata is filled again. 2. UI Migration Notes since the last Broker Release: https://github.com/sovity/edc-ui/releases/tag/v2.0.0 -3. The Protocol Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/ids`~~. -4. The Management Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/management`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/management`~~. -5. The Connector Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to be `https://[MY_EDC_FQDN]/backend/api/v1/ids/data`~~. +3. The Protocol Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to + be `https://[MY_EDC_FQDN]/backend/api/v1/ids`~~. +4. The Management Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/management`, ~~used to + be `https://[MY_EDC_FQDN]/backend/api/v1/management`~~. +5. The Connector Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to + be `https://[MY_EDC_FQDN]/backend/api/v1/ids/data`~~. #### Compatible Versions @@ -258,6 +286,7 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde - Fixed Backend Docker Healthcheck ### Deployment Migration Notes + 1. Added new **required** configuration properties: ```yaml # Broker Server Admin Api Key (required) @@ -298,14 +327,14 @@ Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be adde --header 'x-api-key: ApiKeyDefaultValue' \ --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' ``` - + #### Compatible Versions - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.1` - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` - Sovity EDC CE: [`4.0.1`](https://github.com/sovity/edc-extensions/tree/v4.0.1/connector) -## [v1.0.0] +## [v1.0.0] Release was deleted in favor of above release. There was a bug, and we just decided to re-do the release. diff --git a/connector/.env b/connector/.env index f47574cf3..ca37ee84c 100644 --- a/connector/.env +++ b/connector/.env @@ -71,7 +71,9 @@ EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 # =========================================================== # Ports and Paths -MY_EDC_NAME_KEBAB_CASE=broker +MY_EDC_PARTICIPANT_ID=broker +EDC_CONNECTOR_NAME=${MY_EDC_PARTICIPANT_ID:-MY_EDC_NAME_KEBAB_CASE} +EDC_PARTICIPANT_ID=${MY_EDC_PARTICIPANT_ID:-MY_EDC_NAME_KEBAB_CASE} MY_EDC_BASE_PATH=/backend MY_EDC_PROTOCOL=https:// WEB_HTTP_PORT=11001 @@ -96,7 +98,7 @@ EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD # Oauth default configurations for compatibility with sovity DAPS EDC_OAUTH_PROVIDER_AUDIENCE=${EDC_OAUTH_TOKEN_URL} EDC_OAUTH_ENDPOINT_AUDIENCE=idsc:IDS_CONNECTORS_ALL -EDC_AGENT_IDENTITY_KEY=sub +EDC_AGENT_IDENTITY_KEY=referringConnector # This file could contain an entry replacing the EDC_KEYSTORE ENV var, # but for some reason it is required, and EDC won't start up if it isn't configured. diff --git a/gradle.properties b/gradle.properties index 2262570d6..54c469ed4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=6.0.0 +sovityEdcExtensionsVersion=7.0.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From ea8c1fb9762a99f84e1860b5827843a276aa79ec Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 6 Dec 2023 17:19:56 +0100 Subject: [PATCH 168/295] chore: update docker-compose.yaml --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 54c521d5d..e1f5501d0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -34,6 +34,7 @@ services: EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work ports: - '11001:11001' - '11002:11002' @@ -87,6 +88,7 @@ services: EDC_WEB_REST_CORS_ENABLED: 'true' EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work ports: - '22001:11001' - '22002:11002' From 9db9003479bcbd0bdfacc1a7859b9afb118f30e9 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 6 Dec 2023 17:32:39 +0100 Subject: [PATCH 169/295] ci: try fix parallelity issues (#333) --- extensions/broker-server/build.gradle.kts | 7 +++++++ .../java/de/sovity/edc/ext/brokerserver/TestUtils.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 0e92eb5e6..444e5071a 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -1,5 +1,6 @@ plugins { `java-library` + id("org.gradle.test-retry") version "1.5.7" } val edcVersion: String by project @@ -65,6 +66,12 @@ dependencies { tasks.getByName("test") { useJUnitPlatform() + maxParallelForks = 1 + retry { + maxRetries.set(2) + maxFailures.set(4) + failOnPassedAfterRetry.set(false) + } } tasks.register("prepareKotlinBuildScriptModel") {} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 2d797c6c6..9fba11ee9 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -62,7 +62,7 @@ public static Map createConfiguration( config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); config.put("edc.participant.id", PARTICIPANT_ID); - config.put("my.edc.name.kebab.case", PARTICIPANT_ID); + config.put("my.edc.participant.id", PARTICIPANT_ID); config.put("my.edc.title", "My Connector"); config.put("my.edc.description", "My Connector Description"); config.put("my.edc.curator.url", "https://connector.my-org"); From 8958ea4f6da51945c538697f130b2757cf739e46 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 6 Dec 2023 18:08:50 +0100 Subject: [PATCH 170/295] docs: add referringConnector claim to initial deployment documentation --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a15894ec8..5ead661d4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - +v @@ -157,6 +157,9 @@ For that you will need a SKI/AKI ClientID. Please refer to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-extensions/tree/main/docs/getting-started#faq) on how to generate one. +The DAPS needs to contain the claim `referringConnector=broker` for the broker. +The expected value `broker` could be overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. + ```yaml # Required: Fully Qualified Domain Name MY_EDC_FQDN: "example.com" From 48cd207683517f3838a9089a804419f7b614e63a Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:14:41 +0100 Subject: [PATCH 171/295] fix: Add downwards compatibility for AP API (#337) --- CHANGELOG.md | 4 +- .../api/BrokerServerResource.java | 10 +++++ .../api/model/DataOfferCountResult.java | 21 ++++++++++ .../broker-server-api/client-ts/README.md | 2 +- .../BrokerServerResourceImpl.java | 6 +++ ...rityPortalConnectorMetadataApiService.java | 18 +++++++++ ...horityPortalConnectorMetadataApiTest.java} | 40 ++++++++++++++++++- 7 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java rename extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/{ConnectorMetadataApiTest.java => AuthorityPortalConnectorMetadataApiTest.java} (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f1d1080..d9ceb3587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor -#### Patch +- Authority Portal API: Re-added the deprecated data offer endpoint: `authority-portal-api/data-offer-counts` ### Deployment Migration Notes @@ -49,7 +49,7 @@ EDC 0 / MDS 2.0 bugfix release, Authority Portal API Connector Metadata Endpoint - The DAPS needs to contain the claim `referringConnector=broker` for the broker. The expected value `broker` could be overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. - Authority Portal API: The data offer count endpoint was removed in favor of the new Connector Metadata - Endpoint: `wrapper/broker/authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. + Endpoint: `authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. #### Compatible Versions diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index d87b4f68f..d0fcede7b 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -21,6 +21,7 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import io.swagger.v3.oas.annotations.Operation; @@ -86,4 +87,13 @@ public interface BrokerServerResource { @Produces(MediaType.APPLICATION_JSON) @Operation(description = "Provide Connector metadata by provided Connector Endpoints") List getConnectorMetadata(List endpoints, @QueryParam("adminApiKey") String adminApiKey); + + @POST + @Deprecated + @Path("authority-portal-api/data-offer-counts") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Query the amount of public Data Offers by provided Connector URLs." + + "This endpoint has been replaced by the Authority Portal Connector Metadata endpoint and will be removed in the near future.") + DataOfferCountResult dataOfferCount(List endpoints); } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java new file mode 100644 index 000000000..061644ef3 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java @@ -0,0 +1,21 @@ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Number of Data Offers per Connector endpoint.", requiredMode = Schema.RequiredMode.REQUIRED) +public class DataOfferCountResult { + @Schema(description = "Map from endpoint URL to Data Offer count", requiredMode = Schema.RequiredMode.REQUIRED) + private Map dataOfferCount; +} diff --git a/extensions/broker-server-api/client-ts/README.md b/extensions/broker-server-api/client-ts/README.md index ad2997a92..85741fb43 100644 --- a/extensions/broker-server-api/client-ts/README.md +++ b/extensions/broker-server-api/client-ts/README.md @@ -38,7 +38,7 @@ import { } from '@sovity.de/broker-server-client'; const brokerServerClient: BrokerServerClient = buildBrokerServerClient({ - managementApiUrl: 'http://localhost:11002/api/v1/management', + managementApiUrl: 'http://localhost:11002/api/management', managementApiKey: 'ApiKeyDefaultValue', }); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 0bdd80d23..040a649bf 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -22,6 +22,7 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; @@ -88,4 +89,9 @@ public List getConnectorMetadata(List endp adminApiKeyValidator.validateAdminApiKey(adminApiKey); return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.getMetadataByEndpoints(dsl, endpoints)); } + + @Override + public DataOfferCountResult dataOfferCount(List endpoints) { + return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.countByEndpoints(dsl, endpoints)); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java index 60d103bef..68d726f14 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java @@ -15,10 +15,17 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; +import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; +import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; +import org.jooq.impl.DSL; import java.util.List; +import java.util.function.Function; + +import static java.util.stream.Collectors.toMap; @RequiredArgsConstructor public class AuthorityPortalConnectorMetadataApiService { @@ -37,4 +44,15 @@ public List getMetadataByEndpoints(DSLContext dsl, )) .toList(); } + + public DataOfferCountResult countByEndpoints(DSLContext dsl, List endpoints) { + var connectorMetadata = getMetadataByEndpoints(dsl, endpoints); + + var numDataOffers = connectorMetadata.stream().distinct().collect(toMap( + AuthorityPortalConnectorInfo::getConnectorEndpoint, + AuthorityPortalConnectorInfo::getDataOfferCount + )); + + return new DataOfferCountResult(numDataOffers); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorMetadataApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java similarity index 83% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorMetadataApiTest.java rename to extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java index 26cd62871..51eb4f195 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorMetadataApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java @@ -44,7 +44,7 @@ @ApiTest @ExtendWith(EdcExtension.class) -class ConnectorMetadataApiTest { +class AuthorityPortalConnectorMetadataApiTest { @RegisterExtension private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); @@ -100,6 +100,44 @@ void testConnectorMetadataByEndpoints() { }); } + @Test + void testCountByEndpoints() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var now = OffsetDateTime.now().withNano(0); + + createConnector(dsl, now, 1); + createDataOffer(dsl, now, 1, 1); + createDataOffer(dsl, now, 1, 2); + + createConnector(dsl, now, 2); + createDataOffer(dsl, now, 2, 1); + + createConnector(dsl, now, 3); + createDataOffer(dsl, now, 3, 1); + + createConnector(dsl, now, 4); + + // act + var actual = brokerServerClient().brokerServerApi().dataOfferCount( + Arrays.asList( + getEndpoint(1), + getEndpoint(1), // having this twice should not crash the query + getEndpoint(2), + getEndpoint(4), + getEndpoint(5) // having this not existing should not crash the query + )); + + // assert + var dataOfferCountMap = actual.getDataOfferCount(); + assertThat(dataOfferCountMap).isEqualTo(Map.of( + getEndpoint(1), 2, + getEndpoint(2), 1, + getEndpoint(4), 0 + )); + }); + } + private AuthorityPortalConnectorInfo forConnector(List actual, int iConnector) { return actual.stream() .filter(connectorMetadata -> From a8be04af0af6f161c1b0653ae8c8e39b794c7ba2 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 8 Dec 2023 10:27:30 +0100 Subject: [PATCH 172/295] chore: prepare release (#338) --- .env | 2 +- CHANGELOG.md | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 7b73ad7d8..940969bec 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.0.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.1.0 EDC_IMAGE=ghcr.io/sovity/edc-dev:7.0.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index d9ceb3587..02a0f6624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor -- Authority Portal API: Re-added the deprecated data offer endpoint: `authority-portal-api/data-offer-counts` - ### Deployment Migration Notes #### Compatible Versions @@ -25,6 +23,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v3.1.0] - 2023-08-12 + +### Overview + +Re-added deprecated endpoints for Authority Portal API backward compatibility. + +### Detailed Changes + +#### Minor + +- Authority Portal API: Removed data offer count endpoint in favor of new Connector Metadata Endpoint. + +### Deployment Migration Notes + +_No special migration steps required._ + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.1.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.2.0` +- Sovity EDC CE: [`7.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.0.0) + ## [v3.0.0] - 2023-06-12 ### Overview @@ -46,7 +66,8 @@ EDC 0 / MDS 2.0 bugfix release, Authority Portal API Connector Metadata Endpoint ### Deployment Migration Notes -- The DAPS needs to contain the claim `referringConnector=broker` for the broker. The expected value `broker` could be overridden by +- The DAPS needs to contain the claim `referringConnector=broker` for the broker. The expected value `broker` could be + overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. - Authority Portal API: The data offer count endpoint was removed in favor of the new Connector Metadata Endpoint: `authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. From 4bacb4cb6c84c515cf1004346c752b35223e1de6 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Fri, 8 Dec 2023 10:37:53 +0100 Subject: [PATCH 173/295] chore: improve release.md (#340) --- .github/ISSUE_TEMPLATE/release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index b56e6b6bc..08e3abc83 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -33,7 +33,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Wait for the main branch to be green. - [ ] Test the `docker-compose.yaml` with `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main`. - [ ] Create a release and re-use the changelog section as release description, and the version as title. -- [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). +- [ ] Check if the pipeline built the release versions in the [Actions-Section](https://github.com/sovity/edc-broker-server-extension/actions?query=event%3Arelease) (or you won't see it). - [ ] Checkout the release tag and check test the `docker-compose.yaml`. - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. - [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. From b0775de95c9192d3f7ce3d6b169070df2aece187 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Thu, 21 Dec 2023 15:49:52 +0100 Subject: [PATCH 174/295] chore: remove typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ead661d4..cf6f85a2c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -v + From 1c7e5add6eb55d376beb55e5e07d30d2d19949e4 Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:53:37 +0100 Subject: [PATCH 175/295] feat: bump edc-extensions version for short description text (#352) --- .../brokerserver/BrokerServerExtensionContextBuilder.java | 8 +++++++- gradle.properties | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 3180d56a3..e5e30367b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -85,7 +85,9 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; import de.sovity.edc.utils.catalog.DspCatalogService; import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; @@ -176,9 +178,13 @@ public static BrokerServerExtensionContext buildContext( ); var edcPropertyUtils = new EdcPropertyUtils(); var assetJsonLdUtils = new AssetJsonLdUtils(); + var markdownToTextConverter = new MarkdownToTextConverter(); + var textUtils = new TextUtils(); var uiAssetMapper = new UiAssetMapper( edcPropertyUtils, - assetJsonLdUtils + assetJsonLdUtils, + markdownToTextConverter, + textUtils ); var assetMapper = new AssetMapper( typeTransformerRegistry, diff --git a/gradle.properties b/gradle.properties index 54c469ed4..69a547ff1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.0.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 2a5cab8fe06157ed5ff249bef6d3ba9ca0220d80 Mon Sep 17 00:00:00 2001 From: jkbquabeck <139474964+jkbquabeck@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:44:58 +0100 Subject: [PATCH 176/295] updated MDS feature template (#355) * updated MDS feature template * Iterated the design * Update mds_feature_request.md --------- Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/mds_feature_request.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/mds_feature_request.md b/.github/ISSUE_TEMPLATE/mds_feature_request.md index 4b55f531a..918890e56 100644 --- a/.github/ISSUE_TEMPLATE/mds_feature_request.md +++ b/.github/ISSUE_TEMPLATE/mds_feature_request.md @@ -26,6 +26,14 @@ _What problems does that user face that existing functionalities do solve?_ _Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new innovative ideas?_ +## Scope check + +_Is this feature part of the contracted scope?_ +- [ ] It is part of the contracted scope +- [ ] It is not part of the contracted scope + +If not, please add the label "mds/future-scope" + ## (For sovity Team to complete) Stakeholders _Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ From 787cf146d27856360d59efc6cb24cda516e9bf03 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:59:40 +0100 Subject: [PATCH 177/295] feat: Add organization metadata to broker (#353) --- CHANGELOG.md | 3 + .../api/BrokerServerResource.java | 9 +- .../AuthorityPortalOrganizationMetadata.java | 23 +++ ...rityPortalOrganizationMetadataRequest.java | 21 +++ .../api/model/ConnectorDetailPageResult.java | 3 + .../api/model/ConnectorListEntry.java | 3 + .../migration/V7__Organization_Metadata.sql | 11 ++ .../BrokerServerExtensionContextBuilder.java | 5 +- .../BrokerServerResourceImpl.java | 9 ++ .../catalog/CatalogQueryDataOfferFetcher.java | 1 + .../dao/pages/catalog/CatalogQueryFields.java | 17 +++ .../catalog/models/DataOfferListEntryRs.java | 1 + .../ConnectorDetailQueryService.java | 2 + .../connector/ConnectorListQueryService.java | 8 +- .../connector/model/ConnectorDetailsRs.java | 1 + .../connector/model/ConnectorListEntryRs.java | 1 + .../DataOfferDetailPageQueryService.java | 1 + .../dataoffer/model/DataOfferDetailRs.java | 1 + ...yPortalOrganizationMetadataApiService.java | 44 ++++++ .../services/api/CatalogApiService.java | 3 +- .../api/ConnectorDetailApiService.java | 1 + .../services/api/ConnectorListApiService.java | 1 + .../api/DataOfferDetailApiService.java | 3 +- .../services/api/DataOfferMappingUtils.java | 6 +- .../api/filtering/CatalogFilterService.java | 4 +- .../api/filtering/CatalogSearchService.java | 4 +- .../ConnectorUpdateSuccessWriter.java | 2 + .../ext/brokerserver/utils/MdsIdUtils.java | 29 ++++ .../edc/ext/brokerserver/TestUtils.java | 2 +- ...rityPortalOrganizationMetadataApiTest.java | 117 +++++++++++++++ .../services/api/CatalogApiTest.java | 136 +++++++++++++++++- .../refreshing/ConnectorUpdaterTest.java | 18 ++- 32 files changed, 471 insertions(+), 19 deletions(-) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java create mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a0f6624..ee79aa828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor +- Added an endpoint for syncing organization metadata from the Authority Portal. +- Added organization information to connectors and data offers. + ### Deployment Migration Notes #### Compatible Versions diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index d0fcede7b..1ff7f40af 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -14,13 +14,14 @@ package de.sovity.edc.ext.brokerserver.api; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; @@ -96,4 +97,10 @@ public interface BrokerServerResource { @Operation(description = "Query the amount of public Data Offers by provided Connector URLs." + "This endpoint has been replaced by the Authority Portal Connector Metadata endpoint and will be removed in the near future.") DataOfferCountResult dataOfferCount(List endpoints); + + @POST + @Path("authority-portal-api/organization-metadata") + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Update organization metadata. Organizations not contained in the payload will be deleted.") + void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest organizationMetadataRequest, @QueryParam("adminApiKey") String adminApiKey); } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java new file mode 100644 index 000000000..865e499e2 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java @@ -0,0 +1,23 @@ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Information about a single organization from the Authority Portal.") +public class AuthorityPortalOrganizationMetadata { + @Schema(description = "MDS-ID from the Authority Portal") + private String mdsId; + @Schema(description = "Company name") + private String name; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java new file mode 100644 index 000000000..369a13738 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java @@ -0,0 +1,21 @@ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Information about organizations from the Authority Portal.") +public class AuthorityPortalOrganizationMetadataRequest { + @Schema(description = "Organization metadata") + private List organizations; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java index 74c7c2710..704d8d6cb 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java @@ -36,6 +36,9 @@ public class ConnectorDetailPageResult { @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) private String endpoint; + @Schema(description = "Name of the responsible organization", requiredMode = Schema.RequiredMode.REQUIRED) + private String organizationName; + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) private OffsetDateTime createdAt; diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java index 47ee2a661..d37dbcc78 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java @@ -36,6 +36,9 @@ public class ConnectorListEntry { @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) private String endpoint; + @Schema(description = "Name of the responsible organization", requiredMode = Schema.RequiredMode.REQUIRED) + private String organizationName; + @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) private OffsetDateTime createdAt; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql new file mode 100644 index 000000000..d3e19bca0 --- /dev/null +++ b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql @@ -0,0 +1,11 @@ +-- Create table for organization metadata +create table organization_metadata +( + mds_id text not null primary key, + name text not null +); + +-- Add MDS-ID column to organization table +alter table connector add column mds_id text; +update connector set mds_id = split_part(participant_id, '.', 1) +where participant_id ~ '^MDSL[A-Za-z0-9]+\.C[A-Za-z0-9]+$'; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index e5e30367b..c1360426e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -40,6 +40,7 @@ import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorQueryService; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; @@ -296,6 +297,7 @@ public static BrokerServerExtensionContext buildContext( ); var connectorDetailApiService = new ConnectorDetailApiService(connectorDetailQueryService, connectorOnlineStatusMapper); var connectorListApiService = new ConnectorListApiService(connectorListQueryService, connectorOnlineStatusMapper, paginationMetadataUtils); + var authorityPortalOrganizationMetadataApiService = new AuthorityPortalOrganizationMetadataApiService(); var brokerServerResource = new BrokerServerResourceImpl( dslContextFactory, connectorApiService, @@ -304,7 +306,8 @@ public static BrokerServerExtensionContext buildContext( catalogApiService, dataOfferDetailApiService, adminApiKeyValidator, - dataOfferCountApiService + dataOfferCountApiService, + authorityPortalOrganizationMetadataApiService ); return new BrokerServerExtensionContext( diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 040a649bf..4b9c4f575 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -16,6 +16,7 @@ import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; @@ -27,6 +28,7 @@ import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; @@ -51,6 +53,7 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final DataOfferDetailApiService dataOfferDetailApiService; private final AdminApiKeyValidator adminApiKeyValidator; private final AuthorityPortalConnectorMetadataApiService authorityPortalConnectorMetadataApiService; + private final AuthorityPortalOrganizationMetadataApiService authorityPortalOrganizationMetadataApiService; @Override public CatalogPageResult catalogPage(CatalogPageQuery query) { @@ -94,4 +97,10 @@ public List getConnectorMetadata(List endp public DataOfferCountResult dataOfferCount(List endpoints) { return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.countByEndpoints(dsl, endpoints)); } + + @Override + public void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest organizationMetadataRequest, String adminApiKey) { + adminApiKeyValidator.validateAdminApiKey(adminApiKey); + dslContextFactory.transaction(dsl -> authorityPortalOrganizationMetadataApiService.setOrganizationMetadata(dsl, organizationMetadataRequest.getOrganizations())); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java index 8c98af7b2..24cbb3282 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java @@ -63,6 +63,7 @@ public Field> queryDataOffers( c.ENDPOINT.as("connectorEndpoint"), c.ONLINE_STATUS.as("connectorOnlineStatus"), c.PARTICIPANT_ID.as("connectorParticipantId"), + fields.getOrganizationName().as("organizationName"), fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt") ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java index da19faf16..747f0707d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.catalog; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOfferViewCount; @@ -27,6 +28,8 @@ import java.time.OffsetDateTime; +import static org.jooq.impl.DSL.coalesce; + /** * Tables and fields used in the catalog page query. *

@@ -103,10 +106,24 @@ public Field getViewCount() { return subquery.asField(); } + public Field getOrganizationName() { + return organizationName(connectorTable.MDS_ID); + } + public static Field offlineSinceOrLastUpdatedAt(Connector connectorTable) { return DSL.coalesce( connectorTable.LAST_SUCCESSFUL_REFRESH_AT, connectorTable.CREATED_AT ); } + + public static Field organizationName(Field mdsId) { + var om = Tables.ORGANIZATION_METADATA; + var organizationName = DSL.select(om.NAME) + .from(om) + .where(om.MDS_ID.eq(mdsId)) + .asField() + .cast(String.class); + return coalesce(organizationName, "Unknown"); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java index 8d177cb2e..1c97cf430 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java @@ -36,5 +36,6 @@ public class DataOfferListEntryRs { String connectorEndpoint; ConnectorOnlineStatus connectorOnlineStatus; String connectorParticipantId; + String organizationName; OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java index 336256464..f4acc2589 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.connector; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; @@ -32,6 +33,7 @@ public ConnectorDetailsRs queryConnectorDetailPage(DSLContext dsl, String connec return dsl.select( c.ENDPOINT.as("endpoint"), c.PARTICIPANT_ID.as("participantId"), + CatalogQueryFields.organizationName(c.MDS_ID).as("organizationName"), c.CREATED_AT.as("createdAt"), c.LAST_SUCCESSFUL_REFRESH_AT.as("lastSuccessfulRefreshAt"), c.LAST_REFRESH_ATTEMPT_AT.as("lastRefreshAttemptAt"), diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java index c515c32b3..524631b69 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver.dao.pages.connector; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; import de.sovity.edc.ext.brokerserver.db.jooq.Tables; @@ -32,11 +33,16 @@ public class ConnectorListQueryService { public List queryConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { var c = Tables.CONNECTOR; - var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of(c.ENDPOINT, c.PARTICIPANT_ID)); + var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( + c.ENDPOINT, + c.PARTICIPANT_ID, + CatalogQueryFields.organizationName(c.MDS_ID) + )); return dsl.select( c.ENDPOINT.as("endpoint"), c.PARTICIPANT_ID.as("participantId"), + CatalogQueryFields.organizationName(c.MDS_ID).as("organizationName"), c.CREATED_AT.as("createdAt"), c.LAST_SUCCESSFUL_REFRESH_AT.as("lastSuccessfulRefreshAt"), c.LAST_REFRESH_ATTEMPT_AT.as("lastRefreshAttemptAt"), diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java index a593dcece..071d8e5ce 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java @@ -28,6 +28,7 @@ public class ConnectorDetailsRs { String endpoint; String participantId; + String organizationName; OffsetDateTime createdAt; OffsetDateTime lastSuccessfulRefreshAt; OffsetDateTime lastRefreshAttemptAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java index 189f01c26..96ba710f6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java @@ -28,6 +28,7 @@ public class ConnectorListEntryRs { String endpoint; String participantId; + String organizationName; OffsetDateTime createdAt; OffsetDateTime lastSuccessfulRefreshAt; OffsetDateTime lastRefreshAttemptAt; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java index 1ae809722..f597ac124 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java @@ -49,6 +49,7 @@ public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetI c.ENDPOINT.as("connectorEndpoint"), c.ONLINE_STATUS.as("connectorOnlineStatus"), c.PARTICIPANT_ID.as("connectorParticipantId"), + fields.getOrganizationName().as("organizationName"), fields.getViewCount().as("viewCount")) .from(d) .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java index 54953edb8..0b575849c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java @@ -35,6 +35,7 @@ public class DataOfferDetailRs { String connectorEndpoint; ConnectorOnlineStatus connectorOnlineStatus; String connectorParticipantId; + String organizationName; OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; Integer viewCount; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java new file mode 100644 index 000000000..84086463b --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadata; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.OrganizationMetadataRecord; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import java.util.List; + +@RequiredArgsConstructor +public class AuthorityPortalOrganizationMetadataApiService { + + public void setOrganizationMetadata(DSLContext dsl, List organizationMetadata) { + var records = organizationMetadata.stream().map(this::buildRecord).toList(); + + dsl.deleteFrom(Tables.ORGANIZATION_METADATA).execute(); + dsl.batchInsert(records).execute(); + } + + @NotNull + private OrganizationMetadataRecord buildRecord(AuthorityPortalOrganizationMetadata it) { + var record = new OrganizationMetadataRecord(); + record.setMdsId(it.getMdsId()); + record.setName(it.getName()); + + return record; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java index 0c694c779..d83d1cef5 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java @@ -92,7 +92,8 @@ private CatalogDataOffer buildCatalogDataOffer(DataOfferListEntryRs dataOfferRs) var asset = dataOfferMappingUtils.buildUiAsset( dataOfferRs.getAssetJsonLd(), dataOfferRs.getConnectorEndpoint(), - dataOfferRs.getConnectorParticipantId() + dataOfferRs.getConnectorParticipantId(), + dataOfferRs.getOrganizationName() ); var dataOffer = new CatalogDataOffer(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java index 33ae006e8..f44097b6e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java @@ -34,6 +34,7 @@ public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDe var dto = new ConnectorDetailPageResult(); dto.setParticipantId(connectorDbRow.getParticipantId()); dto.setEndpoint(connectorDbRow.getEndpoint()); + dto.setOrganizationName(connectorDbRow.getOrganizationName()); dto.setCreatedAt(connectorDbRow.getCreatedAt()); dto.setLastRefreshAttemptAt(connectorDbRow.getLastRefreshAttemptAt()); dto.setLastSuccessfulRefreshAt(connectorDbRow.getLastSuccessfulRefreshAt()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java index 6c1af7645..ca0a8d965 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java @@ -60,6 +60,7 @@ private ConnectorListEntry buildConnectorListEntry(ConnectorListEntryRs connecto var dto = new ConnectorListEntry(); dto.setParticipantId(connector.getParticipantId()); dto.setEndpoint(connector.getEndpoint()); + dto.setOrganizationName(connector.getOrganizationName()); dto.setCreatedAt(connector.getCreatedAt()); dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java index e6a927a7f..a79bd8b96 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java @@ -41,7 +41,8 @@ public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDe var asset = dataOfferMappingUtils.buildUiAsset( dataOffer.getAssetJsonLd(), dataOffer.getConnectorEndpoint(), - dataOffer.getConnectorParticipantId() + dataOffer.getConnectorParticipantId(), + dataOffer.getOrganizationName() ); viewCountLogger.increaseDataOfferViewCount(dsl, query.getAssetId(), query.getConnectorEndpoint()); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java index e683bf298..77f0cd8f4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java @@ -26,9 +26,11 @@ public class DataOfferMappingUtils { private final PolicyMapper policyMapper; private final AssetMapper assetMapper; - public UiAsset buildUiAsset(String assetJsonLd, String endpoint, String participantId) { + public UiAsset buildUiAsset(String assetJsonLd, String endpoint, String participantId, String organizationName) { var asset = assetMapper.buildAsset(JsonUtils.parseJsonObj(assetJsonLd)); - return assetMapper.buildUiAsset(asset, endpoint, participantId); + var uiAsset = assetMapper.buildUiAsset(asset, endpoint, participantId); + uiAsset.setCreatorOrganizationName(organizationName); + return uiAsset; } public UiPolicy buildUiPolicy(String policyJson) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index 094c2cdfa..19448368d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -19,10 +19,10 @@ import de.sovity.edc.ext.brokerserver.api.model.CnfFilterItem; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValue; import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValueAttribute; +import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; @@ -84,7 +84,7 @@ private List getAvailableFilters() { "Geo Reference Method" ), catalogFilterAttributeDefinitionService.forField( - fields -> fields.getDataOfferTable().CURATOR_ORGANIZATION_NAME, + CatalogQueryFields::getOrganizationName, "curatorOrganizationName", "Organization Name" ), diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java index 0c4c89bfa..b7abd8b6e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java @@ -33,7 +33,9 @@ public Condition filterBySearch(CatalogQueryFields fields, String searchQuery) { fields.getDataOfferTable().DESCRIPTION, fields.getDataOfferTable().CURATOR_ORGANIZATION_NAME, fields.getDataOfferTable().KEYWORDS_COMMA_JOINED, - fields.getConnectorTable().ENDPOINT + fields.getConnectorTable().ENDPOINT, + fields.getConnectorTable().PARTICIPANT_ID, + fields.getOrganizationName() )); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java index 8b4279b64..51172fd21 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java @@ -21,6 +21,7 @@ import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; +import de.sovity.edc.ext.brokerserver.utils.MdsIdUtils; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -69,6 +70,7 @@ private static void updateConnector(ConnectorRecord connector, FetchedCatalog ca connector.setLastRefreshAttemptAt(now); if (!Objects.equals(connector.getParticipantId(), participantId)) { connector.setParticipantId(participantId); + connector.setMdsId(MdsIdUtils.getMdsIdFromParticipantId(participantId)); changes.setParticipantIdChanged(participantId); } connector.update(); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java new file mode 100644 index 000000000..8bc3fb182 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MdsIdUtils { + public static String getMdsIdFromParticipantId(String participantId) { + if (participantId == null || !participantId.matches("^MDSL[A-Za-z0-9]+\\.C[A-Za-z0-9]+")) { + return null; + } + + return participantId.split("\\.")[0]; + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java index 9fba11ee9..54a2218d9 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java @@ -41,7 +41,7 @@ public class TestUtils { public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH; - public static final String PARTICIPANT_ID = "my-edc-participant-id"; + public static final String PARTICIPANT_ID = "MDSL1234ZZ.C4321AA"; public static final String CURATOR_NAME = "My Org"; @NotNull diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java new file mode 100644 index 000000000..fa907770b --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalOrganizationMetadata; +import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalOrganizationMetadataRequest; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.OrganizationMetadataRecord; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.jooq.DSLContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class AuthorityPortalOrganizationMetadataApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + } + + @Test + void testSetOrganizationMetadata() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + createOrgMetadataInDb(dsl, "MDSL1111AA", "Test Org A"); + createOrgMetadataInDb(dsl, "MDSL2222BB", "Test Org B"); + createOrgMetadataInDb(dsl, "MDSL3333CC", "Test Org C"); + + // act + var orgMetadataRequest = new AuthorityPortalOrganizationMetadataRequest(); + orgMetadataRequest.setOrganizations(List.of( + buildOrgMetadataRequestEntry("MDSL2222BB", "Test Org B"), + buildOrgMetadataRequestEntry("MDSL3333CC", "Test Org C new"), + buildOrgMetadataRequestEntry("MDSL4444DD", "Test Org D") + )); + + brokerServerClient().brokerServerApi().setOrganizationMetadata( + ADMIN_API_KEY, + orgMetadataRequest + ); + + // assert + var orgMetadata = getOrgMetadataFromDb(dsl); + assertThat(orgMetadata).hasSize(3); + assertThat(orgMetadata).extracting(OrganizationMetadataRecord::getName).containsExactlyInAnyOrder("Test Org B", "Test Org C new", "Test Org D"); + }); + } + + @Test + void testSetEmptyOrganizationMetadata() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + createOrgMetadataInDb(dsl, "MDSL1111AA", "Test Org A"); + + // act + var orgMetadataRequest = new AuthorityPortalOrganizationMetadataRequest(); + orgMetadataRequest.setOrganizations(List.of()); + + brokerServerClient().brokerServerApi().setOrganizationMetadata( + ADMIN_API_KEY, + orgMetadataRequest + ); + + // assert + var orgMetadata = getOrgMetadataFromDb(dsl); + assertThat(orgMetadata).isEmpty(); + }); + } + + private void createOrgMetadataInDb(DSLContext dsl, String mdsId, String name) { + var organizationMetadata = dsl.newRecord(Tables.ORGANIZATION_METADATA); + organizationMetadata.setMdsId(mdsId); + organizationMetadata.setName(name); + organizationMetadata.insert(); + } + + private List getOrgMetadataFromDb(DSLContext dsl) { + return dsl.selectFrom(Tables.ORGANIZATION_METADATA).fetch(); + } + + private AuthorityPortalOrganizationMetadata buildOrgMetadataRequestEntry(String mdsId, String name) { + var orgMetadata = new AuthorityPortalOrganizationMetadata(); + orgMetadata.setMdsId(mdsId); + orgMetadata.setName(name); + return orgMetadata; + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 4b0bf063c..dc16852db 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -218,10 +218,7 @@ void testAvailableFilters_noFilter() { Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", Prop.Mds.DATA_MODEL, "my-data-model", - Prop.Mds.GEO_REFERENCE_METHOD, "my-geo-ref", - Prop.Dcterms.CREATOR, Map.of( - Prop.Foaf.NAME, "my-org" - ) + Prop.Mds.GEO_REFERENCE_METHOD, "my-geo-ref" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( @@ -241,7 +238,8 @@ void testAvailableFilters_noFilter() { Prop.Mds.TRANSPORT_MODE, "" )); - createConnector(dsl, today, "https://my-connector/api/dsp"); + createOrganizationMetadata(dsl, "MDSL123456AA", "Test Org"); + createConnector(dsl, today, "https://my-connector/api/dsp", "MDSL123456AA"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd2); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd3); @@ -300,8 +298,8 @@ void testAvailableFilters_noFilter() { assertThat(geoReferenceMethod.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-geo-ref", ""); var curatorOrganizationName = getAvailableFilter(result, "curatorOrganizationName"); - assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-org", "my-participant-id"); // second value comes from tests mocking - assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-org", "my-participant-id"); + assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getId).containsExactly("Test Org"); + assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("Test Org"); var connectorEndpoint = getAvailableFilter(result, "connectorEndpoint"); assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getId).containsExactly("https://my-connector/api/dsp"); @@ -462,6 +460,118 @@ void testSortingByPopularity() { }); } + @Test + void testFilterByOrgName() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var endpoint1 = "https://my-connector-1/api/dsp"; + createConnector(dsl, today, endpoint1, "MDSL1111AA"); + createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); + + var endpoint2 = "https://my-connector-2/api/dsp"; + createConnector(dsl, today, endpoint2, "MDSL2222BB"); + createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); + + createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); + + // act + var query = new CatalogPageQuery(); + query.setFilter(new CnfFilterValue(List.of( + new CnfFilterValueAttribute("curatorOrganizationName", List.of("Test Org")) + ))); + + var actual = brokerServerClient().brokerServerApi().catalogPage(query); + + // assert + assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint1); + }); + } + + @Test + void testSearchForOrgName() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var endpoint1 = "https://my-connector-1/api/dsp"; + createConnector(dsl, today, endpoint1, "MDSL1111AA"); + createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); + + var endpoint2 = "https://my-connector-2/api/dsp"; + createConnector(dsl, today, endpoint2, "MDSL2222BB"); + createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); + + createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); + + // act + var query = new CatalogPageQuery(); + query.setSearchQuery("tEsT"); + + var actual = brokerServerClient().brokerServerApi().catalogPage(query); + + // assert + assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint1); + }); + } + + @Test + void testFilterByUnknown() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var endpoint1 = "https://my-connector-1/api/dsp"; + createConnector(dsl, today, endpoint1, "MDSL1111AA"); + createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); + + var endpoint2 = "https://my-connector-2/api/dsp"; + createConnector(dsl, today, endpoint2, "MDSL2222BB"); + createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); + + createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); + + // act + var query = new CatalogPageQuery(); + query.setFilter(new CnfFilterValue(List.of( + new CnfFilterValueAttribute("curatorOrganizationName", List.of("Unknown")) + ))); + + var actual = brokerServerClient().brokerServerApi().catalogPage(query); + + // assert + assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint2); + }); + } + + @Test + void testSearchForUnknown() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var endpoint1 = "https://my-connector-1/api/dsp"; + createConnector(dsl, today, endpoint1, "MDSL1111AA"); + createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); + + var endpoint2 = "https://my-connector-2/api/dsp"; + createConnector(dsl, today, endpoint2, "MDSL2222BB"); + createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); + + createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); + + // act + var query = new CatalogPageQuery(); + query.setSearchQuery("uNkN"); + + var actual = brokerServerClient().brokerServerApi().catalogPage(query); + + // assert + assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint2); + }); + } + private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { var dataOffer = dsl.newRecord(Tables.DATA_OFFER); setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); @@ -481,8 +591,13 @@ private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connec } private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { + createConnector(dsl, today, connectorEndpoint, null); + } + + private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, String mdsId) { var connector = dsl.newRecord(Tables.CONNECTOR); connector.setParticipantId("my-connector"); + connector.setMdsId(mdsId); connector.setEndpoint(connectorEndpoint); connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); connector.setCreatedAt(today.minusDays(1)); @@ -493,6 +608,13 @@ private void createConnector(DSLContext dsl, OffsetDateTime today, String connec connector.insert(); } + private void createOrganizationMetadata(DSLContext dsl, String mdsId, String name) { + var organizationMetadata = dsl.newRecord(Tables.ORGANIZATION_METADATA); + organizationMetadata.setMdsId(mdsId); + organizationMetadata.setName(name); + organizationMetadata.insert(); + } + private Policy dummyPolicy() { return Policy.Builder.newInstance() .type(PolicyType.SET) diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index 07ac1e531..77f2b960d 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -27,8 +27,11 @@ import de.sovity.edc.ext.brokerserver.TestUtils; import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.db.TestDatabase; import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import de.sovity.edc.utils.jsonld.vocab.Prop; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -36,6 +39,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; @@ -43,6 +48,7 @@ import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayPolicyEdcGen; import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; @ApiTest class ConnectorUpdaterTest { @@ -87,6 +93,7 @@ void testConnectorUpdate() { // act connectorUpdater.updateConnector(connectorEndpoint); + var connectorPage = brokerServerClient.brokerServerApi().connectorPage(new ConnectorPageQuery()); // assert var catalog = brokerServerClient.brokerServerApi().catalogPage(new CatalogPageQuery()); @@ -111,7 +118,7 @@ void testConnectorUpdate() { assertThat(asset.getTransportMode()).isEqualTo("transportMode"); assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url"); assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); - assertThat(asset.getCreatorOrganizationName()).isEqualTo(TestUtils.CURATOR_NAME); + assertThat(asset.getCreatorOrganizationName()).isEqualTo("Unknown"); assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage"); assertThat(asset.getHttpDatasourceHintsProxyMethod()).isFalse(); assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); @@ -126,6 +133,15 @@ void testConnectorUpdate() { var policy = contractOffer.getContractPolicy(); assertThat(policy.getConstraints()).hasSize(1); AssertionUtils.assertEqualUsingJson(policy.getConstraints().get(0), createAfterYesterdayConstraint()); + + var connector = connectorPage.getConnectors().get(0); + assertThat(connector.getOnlineStatus()).isEqualTo(ConnectorListEntry.OnlineStatusEnum.ONLINE); + assertThat(connector.getParticipantId()).isEqualTo(TestUtils.PARTICIPANT_ID); + assertThat(connector.getOrganizationName()).isEqualTo("Unknown"); + assertThat(connector.getLastRefreshAttemptAt()).isCloseTo(OffsetDateTime.now(), within(1, ChronoUnit.SECONDS)); + + var connectorRecord = dsl.selectFrom(Tables.CONNECTOR).fetchOne(); + assertThat(connectorRecord.getMdsId()).isEqualTo("MDSL1234ZZ"); }); } From bc44117a93d8c3f09100c2d3495d56cbbeb52307 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:13:07 +0100 Subject: [PATCH 178/295] chore: fix dependabot config --- .github/dependabot.yml | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5a0d3f3f9..0224f6451 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,17 +6,17 @@ updates: schedule: interval: "weekly" target-branch: "main" - open-pull-requests-limit: 30 + open-pull-requests-limit: 50 labels: - "area/dependency" - "area/github" # Docker - package-ecosystem: "docker" - directory: "/" + directory: "/connector/" schedule: interval: "daily" target-branch: "main" - open-pull-requests-limit: 30 + open-pull-requests-limit: 50 labels: - "area/dependency" - "area/docker" @@ -26,27 +26,17 @@ updates: schedule: interval: "daily" target-branch: "main" - open-pull-requests-limit: 30 - labels: - - "area/dependency" - - "area/java" - # Maven - - package-ecosystem: "maven" - directory: "/" - schedule: - interval: "daily" - target-branch: "main" - open-pull-requests-limit: 30 + open-pull-requests-limit: 50 labels: - "area/dependency" - "area/java" # NPM - package-ecosystem: "npm" - directory: "/" + directory: "/extensions/broker-server-api/client-ts/" schedule: interval: "daily" target-branch: "main" - open-pull-requests-limit: 30 + open-pull-requests-limit: 50 labels: - "area/dependency" - "area/javascript" From d0484d5443cac1788166349495a816fe25a18d75 Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:49:05 +0100 Subject: [PATCH 179/295] chore: provide dummy OwnConnectorEndpointService (#367) --- .../ext/brokerserver/BrokerServerExtensionContextBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index c1360426e..3c94119b3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -185,7 +185,8 @@ public static BrokerServerExtensionContext buildContext( edcPropertyUtils, assetJsonLdUtils, markdownToTextConverter, - textUtils + textUtils, + endpoint -> false ); var assetMapper = new AssetMapper( typeTransformerRegistry, From 654e265b12ec7138ad1dfb02aa6b9fb58efc210a Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:55:52 +0100 Subject: [PATCH 180/295] chore: prepare release (#371) --- .env | 6 +++--- CHANGELOG.md | 26 +++++++++++++++++++++++--- gradle.properties | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 940969bec..a5b968090 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.1.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.0.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.2.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.2.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.1.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.3.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index ee79aa828..111ac4dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor -- Added an endpoint for syncing organization metadata from the Authority Portal. -- Added organization information to connectors and data offers. - ### Deployment Migration Notes #### Compatible Versions @@ -26,6 +23,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v3.2.0] - 2024-01-18 + +### Overview + +Added validated organization information. + +### Detailed Changes + +#### Minor + +- Validated organization information from the Authority Portal is now displayed +- Authority Portal API: Added endpoint for receiving organization metadata + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.2.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.3.0` +- Sovity EDC CE: [`7.1.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.1.0) + ## [v3.1.0] - 2023-08-12 ### Overview diff --git a/gradle.properties b/gradle.properties index 69a547ff1..a6cc2ed83 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=7.1.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 2d56045947829705e5e5d40ab98074a41238370a Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:00:00 +0100 Subject: [PATCH 181/295] chore: prepare release (2) (#372) --- .env | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env b/.env index a5b968090..a502e342a 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.2.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.1.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.3.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.1.1 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.3.1 diff --git a/gradle.properties b/gradle.properties index a6cc2ed83..59f53db7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.1.0 +sovityEdcExtensionsVersion=7.1.1 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 47d1bad06af5379846da682e07faf92154c251a2 Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:28:04 +0100 Subject: [PATCH 182/295] chore: update docker compose (#373) --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index e1f5501d0..5143f2ba1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -67,7 +67,7 @@ services: depends_on: - connector-postgresql environment: - MY_EDC_NAME_KEBAB_CASE: "my-connector" + MY_EDC_PARTICIPANT_ID: "MDSL00001XX.C0001XX" MY_EDC_TITLE: "EDC Connector" MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" MY_EDC_CURATOR_URL: "https://example.com" From ee2040a406ed50e8701fcf8b914fc7f2a8c35b23 Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:31:22 +0100 Subject: [PATCH 183/295] chore: prepare release (3) (#374) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 111ac4dfa..8b2338454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,8 +43,8 @@ _No special deployment migration steps required_ #### Compatible Versions - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.2.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.3.0` -- Sovity EDC CE: [`7.1.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.1.0) +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.3.1` +- Sovity EDC CE: [`7.1.1`](https://github.com/sovity/edc-extensions/releases/tag/v7.1.1) ## [v3.1.0] - 2023-08-12 From e2b499677c7665d7fca799b59fa5461706f66b2a Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:08:58 +0100 Subject: [PATCH 184/295] feat: bump edc-extensions version for new MDS fields (#381) --- CHANGELOG.md | 1 + .../services/api/CatalogApiTest.java | 32 +++++++++---------- .../services/api/ConnectorApiTest.java | 4 +-- .../services/api/DataOfferDetailApiTest.java | 4 +-- gradle.properties | 2 +- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2338454..321e1bdd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major #### Minor +- Add new MDS fields and migrate existing MDS asset keys to mobilityDCAT-AP ### Deployment Migration Notes diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index dc16852db..222faa9b2 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -214,28 +214,28 @@ void testAvailableFilters_noFilter() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", - Prop.Mds.DATA_MODEL, "my-data-model", - Prop.Mds.GEO_REFERENCE_METHOD, "my-geo-ref" + Prop.Mobility.DATA_CATEGORY, "my-category-1", + Prop.Mobility.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + Prop.Mobility.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", + Prop.Mobility.DATA_MODEL, "my-data-model", + Prop.Mobility.GEO_REFERENCE_METHOD, "my-geo-ref" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "my-transport-mode-2", - Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + Prop.Mobility.DATA_CATEGORY, "my-category-1", + Prop.Mobility.TRANSPORT_MODE, "my-transport-mode-2", + Prop.Mobility.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" )); var assetJsonLd3 = getAssetJsonLd("my-asset-3", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - Prop.Mds.DATA_SUBCATEGORY, "my-subcategory-1" + Prop.Mobility.DATA_CATEGORY, "my-category-1", + Prop.Mobility.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + Prop.Mobility.DATA_SUBCATEGORY, "my-subcategory-1" )); var assetJsonLd4 = getAssetJsonLd("my-asset-4", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "" + Prop.Mobility.DATA_CATEGORY, "my-category-1", + Prop.Mobility.TRANSPORT_MODE, "" )); createOrganizationMetadata(dsl, "MDSL123456AA", "Test Org"); @@ -348,12 +348,12 @@ void testAvailableFilters_withFilter() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", - Prop.Mds.DATA_SUBCATEGORY, "my-subcategory" + Prop.Mobility.DATA_CATEGORY, "my-category", + Prop.Mobility.DATA_SUBCATEGORY, "my-subcategory" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mds.DATA_SUBCATEGORY, "my-other-subcategory" + Prop.Mobility.DATA_SUBCATEGORY, "my-other-subcategory" )); createConnector(dsl, today, "https://my-connector/api/dsp"); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index c567728af..d17f465ef 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -63,7 +63,7 @@ void testQueryConnectors() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Mobility.DATA_CATEGORY, "my-category", Prop.Dcterms.TITLE, "My Asset 1" )); @@ -89,7 +89,7 @@ void testQueryConnectorDetails() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Mobility.DATA_CATEGORY, "my-category", Prop.Dcterms.TITLE, "My Asset 1" )); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index 299742ba7..f2b786b37 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -63,12 +63,12 @@ void testQueryDataOfferDetails() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Mobility.DATA_CATEGORY, "my-category", Prop.Dcterms.TITLE, "My Asset 1" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-2", + Prop.Mobility.DATA_CATEGORY, "my-category-2", Prop.Dcterms.TITLE, "My Asset 2" )); diff --git a/gradle.properties b/gradle.properties index 59f53db7b..69a547ff1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.1.1 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From ae069bfb25816b7f008630a4022ea908a6510286 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:40:04 +0100 Subject: [PATCH 185/295] ci: update license scan (#385) * Update license_scan.yml * Delete .github/license_scan_config.yml * Update license_scan.yml --- .github/license_scan_config.yml | 13 ------------- .github/workflows/license_scan.yml | 26 +++++++++++++++++++++----- 2 files changed, 21 insertions(+), 18 deletions(-) delete mode 100644 .github/license_scan_config.yml diff --git a/.github/license_scan_config.yml b/.github/license_scan_config.yml deleted file mode 100644 index bf7c41855..000000000 --- a/.github/license_scan_config.yml +++ /dev/null @@ -1,13 +0,0 @@ -format: table -vulnerability: - type: - - os - - library - ignore-unfixed: true -scan: - security-checks: - - license -license-full: true -severity: - - HIGH - - CRITICAL diff --git a/.github/workflows/license_scan.yml b/.github/workflows/license_scan.yml index 711695b6e..7a0e07505 100644 --- a/.github/workflows/license_scan.yml +++ b/.github/workflows/license_scan.yml @@ -1,17 +1,16 @@ name: Trivy License Scan on: - pull_request: - branches: ["main"] + push: jobs: - license_scan: - name: license_scan + license_scan1: + name: License scan (rootfs) runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Run license scanner uses: aquasecurity/trivy-action@master @@ -21,3 +20,20 @@ jobs: scanners: "license" severity: "CRITICAL,HIGH" exit-code: 1 + license_scan2: + name: License scan (repo) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: npm install (client-ts) + run: cd extensions/broker-server-api/client-ts && npm install + - name: Run license scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "repo" + scan-ref: "." + scanners: "license" + severity: "CRITICAL,HIGH" + exit-code: 1 From 0d22a3b1c6717eb9ff7c2b871addc35a25d015f3 Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:40:22 +0100 Subject: [PATCH 186/295] chore: prepare release --- .env | 6 ++-- CHANGELOG.md | 26 ++++++++++++--- .../services/api/CatalogApiTest.java | 32 +++++++++---------- .../services/api/ConnectorApiTest.java | 4 +-- .../services/api/DataOfferDetailApiTest.java | 4 +-- gradle.properties | 2 +- 6 files changed, 46 insertions(+), 28 deletions(-) diff --git a/.env b/.env index a502e342a..4d1028189 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.2.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.1.1 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.3.1 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.3.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 321e1bdd9..7461e5d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,15 +14,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major #### Minor -- Add new MDS fields and migrate existing MDS asset keys to mobilityDCAT-AP + +#### Patch + +### Deployment Migration Notes + +#### Compatible Versions + +## [v3.3.0] - 2024-02-14 + +### Overview + +MDS bugfix and feature release + +### Detailed Changes + +#### Minor +- Assets now have new MDS fields ### Deployment Migration Notes +_No special deployment migration steps required_ + #### Compatible Versions -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION }}` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` -- Sovity EDC CE: {{ CE Release Link }} +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.3.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` +- Sovity EDC CE: [`7.2.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.0) ## [v3.2.0] - 2024-01-18 diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index 222faa9b2..dc16852db 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -214,28 +214,28 @@ void testAvailableFilters_noFilter() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category-1", - Prop.Mobility.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - Prop.Mobility.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", - Prop.Mobility.DATA_MODEL, "my-data-model", - Prop.Mobility.GEO_REFERENCE_METHOD, "my-geo-ref" + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", + Prop.Mds.DATA_MODEL, "my-data-model", + Prop.Mds.GEO_REFERENCE_METHOD, "my-geo-ref" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category-1", - Prop.Mobility.TRANSPORT_MODE, "my-transport-mode-2", - Prop.Mobility.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "my-transport-mode-2", + Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" )); var assetJsonLd3 = getAssetJsonLd("my-asset-3", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category-1", - Prop.Mobility.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - Prop.Mobility.DATA_SUBCATEGORY, "my-subcategory-1" + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", + Prop.Mds.DATA_SUBCATEGORY, "my-subcategory-1" )); var assetJsonLd4 = getAssetJsonLd("my-asset-4", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category-1", - Prop.Mobility.TRANSPORT_MODE, "" + Prop.Mds.DATA_CATEGORY, "my-category-1", + Prop.Mds.TRANSPORT_MODE, "" )); createOrganizationMetadata(dsl, "MDSL123456AA", "Test Org"); @@ -348,12 +348,12 @@ void testAvailableFilters_withFilter() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category", - Prop.Mobility.DATA_SUBCATEGORY, "my-subcategory" + Prop.Mds.DATA_CATEGORY, "my-category", + Prop.Mds.DATA_SUBCATEGORY, "my-subcategory" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mobility.DATA_SUBCATEGORY, "my-other-subcategory" + Prop.Mds.DATA_SUBCATEGORY, "my-other-subcategory" )); createConnector(dsl, today, "https://my-connector/api/dsp"); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index d17f465ef..c567728af 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -63,7 +63,7 @@ void testQueryConnectors() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category", + Prop.Mds.DATA_CATEGORY, "my-category", Prop.Dcterms.TITLE, "My Asset 1" )); @@ -89,7 +89,7 @@ void testQueryConnectorDetails() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category", + Prop.Mds.DATA_CATEGORY, "my-category", Prop.Dcterms.TITLE, "My Asset 1" )); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index f2b786b37..299742ba7 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -63,12 +63,12 @@ void testQueryDataOfferDetails() { var today = OffsetDateTime.now().withNano(0); var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category", + Prop.Mds.DATA_CATEGORY, "my-category", Prop.Dcterms.TITLE, "My Asset 1" )); var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mobility.DATA_CATEGORY, "my-category-2", + Prop.Mds.DATA_CATEGORY, "my-category-2", Prop.Dcterms.TITLE, "My Asset 2" )); diff --git a/gradle.properties b/gradle.properties index 69a547ff1..ab3747ec3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=7.2.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From d1fa0528cc964611fc80ca582c60b14404daec6e Mon Sep 17 00:00:00 2001 From: Sebastian Opriel <22075788+SebastianOpriel@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:26:46 +0100 Subject: [PATCH 187/295] Authority Portal API: Added endpoint for receiving all data offers of registered connectors --- CHANGELOG.md | 2 + .../api/BrokerServerResource.java | 8 + ...horityPortalConnectorDataOfferDetails.java | 38 ++++ ...AuthorityPortalConnectorDataOfferInfo.java | 50 +++++ ...thorityPortalConnectorDataOfferResult.java | 21 +++ .../BrokerServerExtensionContextBuilder.java | 3 + .../BrokerServerResourceImpl.java | 9 + ...ityPortalConnectorDataOfferApiService.java | 41 ++++ .../AuthorityPortalConnectorQueryService.java | 48 +++++ .../api/AuthorityPortalDataOfferApiTest.java | 178 ++++++++++++++++++ 10 files changed, 398 insertions(+) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java create mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java create mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7461e5d38..68a20b9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major #### Minor +- Authority Portal API: Added endpoint for receiving all data offers of registered connectors + #### Patch diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index 1ff7f40af..f3228ec40 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.api; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; @@ -103,4 +104,11 @@ public interface BrokerServerResource { @Consumes(MediaType.APPLICATION_JSON) @Operation(description = "Update organization metadata. Organizations not contained in the payload will be deleted.") void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest organizationMetadataRequest, @QueryParam("adminApiKey") String adminApiKey); + + @POST + @Path("authority-portal-api/data-offer-info") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Provides information about Data Offers for given Connectors.") + List getConnectorDataOffers(List endpoints, @QueryParam("adminApiKey") String adminApiKey); } diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java new file mode 100644 index 000000000..4ef3d92af --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Data offer details of a connector.") +public class AuthorityPortalConnectorDataOfferDetails { + @Schema(description = "Asset ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String dataOfferId; + + @Schema(description = "Name of the asset", requiredMode = Schema.RequiredMode.REQUIRED) + private String dataOfferName; + +} + diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java new file mode 100644 index 000000000..13f4792eb --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Details of a Connector and its data offers.") +public class AuthorityPortalConnectorDataOfferInfo { + + @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + + @Schema(description = "ID of participant", requiredMode = Schema.RequiredMode.REQUIRED) + private String participantId; + + @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ConnectorOnlineStatus onlineStatus; + + @Schema(description = "Date to be displayed as last update date, for online connectors it's the last refresh date, for offline connectors it's the creation date or last successful fetch.") + private OffsetDateTime offlineSinceOrLastUpdatedAt; + + @Schema(description = "Available Data Offers", requiredMode = Schema.RequiredMode.REQUIRED) + private List dataOffers; + +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java new file mode 100644 index 000000000..35a0c2d83 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java @@ -0,0 +1,21 @@ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Provides information about connectors with some meta information and data offers.", requiredMode = Schema.RequiredMode.REQUIRED) +public class AuthorityPortalConnectorDataOfferResult { + @Schema(description = "List of connectors containing information about data offers", requiredMode = Schema.RequiredMode.REQUIRED) + private List authorityPortalConnectorDataOfferInfos; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 3c94119b3..11032c61a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -40,6 +40,7 @@ import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorQueryService; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; @@ -299,6 +300,7 @@ public static BrokerServerExtensionContext buildContext( var connectorDetailApiService = new ConnectorDetailApiService(connectorDetailQueryService, connectorOnlineStatusMapper); var connectorListApiService = new ConnectorListApiService(connectorListQueryService, connectorOnlineStatusMapper, paginationMetadataUtils); var authorityPortalOrganizationMetadataApiService = new AuthorityPortalOrganizationMetadataApiService(); + var authorityPortalDataOfferApiService = new AuthorityPortalConnectorDataOfferApiService(connectorQueryService, connectorOnlineStatusMapper); var brokerServerResource = new BrokerServerResourceImpl( dslContextFactory, connectorApiService, @@ -308,6 +310,7 @@ public static BrokerServerExtensionContext buildContext( dataOfferDetailApiService, adminApiKeyValidator, dataOfferCountApiService, + authorityPortalDataOfferApiService, authorityPortalOrganizationMetadataApiService ); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 4b9c4f575..662e013c4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -15,6 +15,7 @@ package de.sovity.edc.ext.brokerserver; import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; @@ -28,6 +29,7 @@ import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; @@ -53,6 +55,7 @@ public class BrokerServerResourceImpl implements BrokerServerResource { private final DataOfferDetailApiService dataOfferDetailApiService; private final AdminApiKeyValidator adminApiKeyValidator; private final AuthorityPortalConnectorMetadataApiService authorityPortalConnectorMetadataApiService; + private final AuthorityPortalConnectorDataOfferApiService authorityPortalConnectorDataOffersApiService; private final AuthorityPortalOrganizationMetadataApiService authorityPortalOrganizationMetadataApiService; @Override @@ -103,4 +106,10 @@ public void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest o adminApiKeyValidator.validateAdminApiKey(adminApiKey); dslContextFactory.transaction(dsl -> authorityPortalOrganizationMetadataApiService.setOrganizationMetadata(dsl, organizationMetadataRequest.getOrganizations())); } + + @Override + public List getConnectorDataOffers(List endpoints, String adminApiKey) { + adminApiKeyValidator.validateAdminApiKey(adminApiKey); + return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorDataOffersApiService.getConnectorDataOffersByEndpoints(dsl, endpoints)); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java new file mode 100644 index 000000000..baf08bc02 --- /dev/null +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; +import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferDetails; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.List; + +@RequiredArgsConstructor +public class AuthorityPortalConnectorDataOfferApiService { + private final AuthorityPortalConnectorQueryService authorityPortalConnectorQueryService; + private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; + + public List getConnectorDataOffersByEndpoints(DSLContext dsl, List endpoints) { + return authorityPortalConnectorQueryService.getConnectorsDataOffers(dsl, endpoints).stream() + .map(it -> new AuthorityPortalConnectorDataOfferInfo( + it.getConnectorEndpoint(), + it.getParticipantId(), + connectorOnlineStatusMapper.getOnlineStatus(it.getOnlineStatus()), + it.getOfflineSinceOrLastUpdatedAt(), + it.getDataOffers().stream().map(dataOffer -> new AuthorityPortalConnectorDataOfferDetails(dataOffer.getDataOfferId(), dataOffer.getDataOfferName())).toList() + )) + .toList(); + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java index d6932baac..bb432ae5c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java @@ -47,6 +47,23 @@ public static class ConnectorMetadataRs { Integer dataOfferCount; } + @Data + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class ConnectorDetailsRs { + String connectorEndpoint; + String participantId; + ConnectorOnlineStatus onlineStatus; + OffsetDateTime offlineSinceOrLastUpdatedAt; + List dataOffers; + } + + @Data + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class DataOfferRs { + String dataOfferId; + String dataOfferName; + } + @NotNull public List getConnectorMetadata(DSLContext dsl, List endpoints) { var c = Tables.CONNECTOR; @@ -72,4 +89,35 @@ public Field getDataOfferCount(Field connectorEndpoint) { .where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)) .asField(); } + + @NotNull + public List getDataOffers(DSLContext dsl, String connectorEndpoint) { + var d = Tables.DATA_OFFER; + + return dsl.select( + d.ASSET_TITLE.as("dataOfferName"), + d.ASSET_ID.as("dataOfferId") + ) + .from(d) + .where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)) + .fetchInto(DataOfferRs.class); + } + + + @NotNull + public List getConnectorsDataOffers(DSLContext dsl, List endpoints) { + var c = Tables.CONNECTOR; + + var connectors = dsl.select( + c.ENDPOINT.as("connectorEndpoint"), + c.PARTICIPANT_ID.as("participantId"), + c.ONLINE_STATUS.as("onlineStatus"), + CatalogQueryFields.offlineSinceOrLastUpdatedAt(c).as("offlineSinceOrLastUpdatedAt") + ) + .from(c) + .where(PostgresqlUtils.in(c.ENDPOINT, endpoints)) + .fetchInto(ConnectorDetailsRs.class); + connectors.forEach(connector -> connector.dataOffers = getDataOffers(dsl, connector.connectorEndpoint)); + return connectors; + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java new file mode 100644 index 000000000..607b77594 --- /dev/null +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.brokerserver.services.api; + +import de.sovity.edc.ext.brokerserver.TestPolicy; +import de.sovity.edc.ext.brokerserver.client.gen.ApiException; +import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalConnectorDataOfferInfo; +import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalConnectorInfo; +import de.sovity.edc.ext.brokerserver.db.TestDatabase; +import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; +import de.sovity.edc.ext.brokerserver.db.jooq.Tables; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.jooq.DSLContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; +import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; +import static de.sovity.edc.ext.brokerserver.TestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +@ApiTest +@ExtendWith(EdcExtension.class) +class AuthorityPortalDataOfferApiTest { + + @RegisterExtension + private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); + + @BeforeEach + void setUp(EdcExtension extension) { + extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); + } + + @Test + void testAuthenticationOfEndpoints() { + TEST_DATABASE.testTransaction(dsl -> { + var code = assertThrows(ApiException.class, () -> brokerServerClient().brokerServerApi().getConnectorDataOffers( + ADMIN_API_KEY + "invalid", + List.of())).getCode(); + assertThat(code).isEqualTo(401); + }); + } + + @Test + void testConnectorMetadataByEndpoints() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var now = OffsetDateTime.now().withNano(0); + + createConnector(dsl, now, 1); + createDataOffer(dsl, now, 1, 1); + createDataOffer(dsl, now, 1, 2); + + createConnector(dsl, now, 2); + createDataOffer(dsl, now, 2, 1); + + createConnector(dsl, now, 4); + + // act + var actual = brokerServerClient().brokerServerApi().getConnectorDataOffers( + ADMIN_API_KEY, + Arrays.asList( + getEndpoint(1), + getEndpoint(2), + getEndpoint(4) + )); + + // assert + // connector 1 with two data offer + var connector1 = forConnector(actual, 1); + assertThat(connector1.getConnectorEndpoint()).isEqualTo(getEndpoint(1)); + assertThat(connector1.getParticipantId()).isEqualTo("my-connector"); + assertThat(connector1.getOnlineStatus().getValue()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE.getValue()); + assertThat(connector1.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); + assertThat(connector1.getDataOffers().size()).isEqualTo(2); + var connector1asset1 = connector1.getDataOffers().stream().filter(dataOffer -> dataOffer.getDataOfferId().equals(getAssetId(1))).findFirst().orElseThrow(); + assertThat(connector1asset1.getDataOfferId()).isEqualTo("my-asset-1"); + assertThat(connector1asset1.getDataOfferName()).isEqualTo("my-asset-1"); + var connector1asset2 = connector1.getDataOffers().stream().filter(dataOffer -> dataOffer.getDataOfferId().equals(getAssetId(2))).findFirst().orElseThrow(); + assertThat(connector1asset2.getDataOfferId()).isEqualTo("my-asset-2"); + assertThat(connector1asset2.getDataOfferName()).isEqualTo("my-asset-2"); + + // connector 2 with one data offer + var connector2 = forConnector(actual, 2); + assertThat(connector2.getConnectorEndpoint()).isEqualTo(getEndpoint(2)); + assertThat(connector2.getParticipantId()).isEqualTo("my-connector"); + assertThat(connector2.getOnlineStatus().getValue()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE.getValue()); + assertThat(connector2.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); + assertThat(connector2.getDataOffers().size()).isEqualTo(1); + var connector2asset1 = connector2.getDataOffers().stream().filter(dataOffer -> dataOffer.getDataOfferId().equals(getAssetId(1))).findFirst().orElseThrow(); + assertThat(connector2asset1.getDataOfferId()).isEqualTo("my-asset-1"); + assertThat(connector2asset1.getDataOfferName()).isEqualTo("my-asset-1"); + + // connector 4 without data offers + var connector4 = forConnector(actual, 4); + assertThat(connector4.getConnectorEndpoint()).isEqualTo(getEndpoint(4)); + assertThat(connector4.getParticipantId()).isEqualTo("my-connector"); + assertThat(connector4.getOnlineStatus().getValue()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE.getValue()); + assertThat(connector4.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); + assertThat(connector4.getDataOffers().size()).isEqualTo(0); + }); + } + + private AuthorityPortalConnectorDataOfferInfo forConnector(List actual, int iConnector) { + return actual.stream() + .filter(connectorMetadata -> + getEndpoint(iConnector).equals(connectorMetadata.getConnectorEndpoint()) + ) + .findFirst() + .orElseThrow(); + } + + private void createConnector(DSLContext dsl, OffsetDateTime now, int iConnector) { + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setParticipantId("my-connector"); + connector.setEndpoint(getEndpoint(iConnector)); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + connector.setCreatedAt(now.minusDays(1)); + connector.setLastRefreshAttemptAt(now); + connector.setLastSuccessfulRefreshAt(now); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + connector.insert(); + } + + private String getEndpoint(int iConnector) { + return "https://connector-%d".formatted(iConnector); + } + + private String getAssetId(int iDataOffer) { + return "my-asset-%d".formatted(iDataOffer); + } + + private void createDataOffer(DSLContext dsl, OffsetDateTime now, int iConnector, int iDataOffer) { + var connectorEndpoint = getEndpoint(iConnector); + var assetJsonLd = getAssetJsonLd(getAssetId(iDataOffer)); + + var dataOffer = dsl.newRecord(Tables.DATA_OFFER); + setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); + dataOffer.setConnectorEndpoint(connectorEndpoint); + dataOffer.setCreatedAt(now.minusDays(5)); + dataOffer.setUpdatedAt(now); + dataOffer.insert(); + + var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); + contractOffer.setContractOfferId("my-contract-offer-1"); + contractOffer.setConnectorEndpoint(connectorEndpoint); + contractOffer.setAssetId(dataOffer.getAssetId()); + contractOffer.setCreatedAt(now.minusDays(5)); + contractOffer.setUpdatedAt(now); + contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); + contractOffer.insert(); + } +} From 174ee06961abaa4d6dbf331c0e91882aac6ea945 Mon Sep 17 00:00:00 2001 From: Sebastian Opriel <22075788+SebastianOpriel@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:56:22 +0100 Subject: [PATCH 188/295] fixed: Wrong lexicographical order --- .../ext/brokerserver/BrokerServerExtensionContextBuilder.java | 2 +- .../sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 11032c61a..6f4b43084 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -38,9 +38,9 @@ import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorQueryService; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 662e013c4..02d57edde 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -28,8 +28,8 @@ import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; +import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; From e7e2a96d07eea6611a840590cf59c21e66be028c Mon Sep 17 00:00:00 2001 From: Sebastian Opriel <22075788+SebastianOpriel@users.noreply.github.com> Date: Thu, 15 Feb 2024 15:31:42 +0100 Subject: [PATCH 189/295] - removed unused file - refactored to explicit imports - Adjusted Markdown syntax --- CHANGELOG.md | 1 + ...thorityPortalConnectorDataOfferResult.java | 21 ------------------- .../api/AuthorityPortalDataOfferApiTest.java | 4 +++- 3 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a20b9d7..045072881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ MDS bugfix and feature release ### Detailed Changes #### Minor + - Assets now have new MDS fields ### Deployment Migration Notes diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java deleted file mode 100644 index 35a0c2d83..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Provides information about connectors with some meta information and data offers.", requiredMode = Schema.RequiredMode.REQUIRED) -public class AuthorityPortalConnectorDataOfferResult { - @Schema(description = "List of connectors containing information about data offers", requiredMode = Schema.RequiredMode.REQUIRED) - private List authorityPortalConnectorDataOfferInfos; -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java index 607b77594..9cd29b55f 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java @@ -39,7 +39,9 @@ import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestUtils.*; +import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; +import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; +import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertThrows; From 0e65d3ff469b8b72414b6129ceae9c9f666954e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:31:17 +0100 Subject: [PATCH 190/295] chore(deps): bump org.postgresql:postgresql from 42.6.0 to 42.7.2 (#424) Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.6.0 to 42.7.2. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/commits) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../broker-server-postgres-flyway-jooq/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 12b88dd30..6956ef074 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -37,8 +37,8 @@ dependencies { api("org.jooq:jooq:3.18.7") api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") - jooqGenerator("org.postgresql:postgresql:42.6.0") - flywayMigration("org.postgresql:postgresql:42.6.0") + jooqGenerator("org.postgresql:postgresql:42.7.2") + flywayMigration("org.postgresql:postgresql:42.7.2") implementation("com.zaxxer:HikariCP:5.0.1") annotationProcessor("org.projectlombok:lombok:1.18.30") From a9ceded10c2524262d0f55b46651cb723dc8c654 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:37:19 +0100 Subject: [PATCH 191/295] fix: add missing license header (#422) * Update AuthorityPortalConnectorInfo.java * Update AuthorityPortalOrganizationMetadata.java * Update AuthorityPortalConnectorInfo.java * Update AuthorityPortalOrganizationMetadataRequest.java --- .../api/model/AuthorityPortalConnectorInfo.java | 13 +++++++++++++ .../model/AuthorityPortalOrganizationMetadata.java | 13 +++++++++++++ .../AuthorityPortalOrganizationMetadataRequest.java | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java index 7a9061cbf..7bc088765 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ package de.sovity.edc.ext.brokerserver.api.model; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java index 865e499e2..5acd846e1 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ package de.sovity.edc.ext.brokerserver.api.model; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java index 369a13738..68eb269eb 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ package de.sovity.edc.ext.brokerserver.api.model; import io.swagger.v3.oas.annotations.media.Schema; From 11991af04e0c24dfceb13b56e6aad41f36fc5651 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 21 Feb 2024 10:22:03 +0100 Subject: [PATCH 192/295] Update security_scan.yml --- .github/workflows/security_scan.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml index 2555ed0ce..9c3f06aa1 100644 --- a/.github/workflows/security_scan.yml +++ b/.github/workflows/security_scan.yml @@ -1,8 +1,8 @@ name: Trivy Security Scan on: - pull_request: - branches: ["main"] + push: + workflow_dispatch: jobs: security_scan: @@ -11,20 +11,19 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Run static analysis + - name: Run static analysis (rootfs) uses: aquasecurity/trivy-action@master with: - scan-type: "fs" - scanners: "vuln,config" + scan-type: "rootfs" + scanners: "vuln,misconfig" ignore-unfixed: true - format: "sarif" - output: "trivy-results.sarif" + format: 'table' severity: "CRITICAL,HIGH" - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 - continue-on-error: true + - name: Run static analysis (repo) + uses: aquasecurity/trivy-action@master with: - sarif_file: "trivy-results.sarif" - category: "code" + scan-type: "repo" + scanners: "vuln,misconfig" + ignore-unfixed: true + format: 'table' + severity: "CRITICAL,HIGH" From aaa18a960c6fb88feed568c1bd045f8e99074c8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:03:24 +0100 Subject: [PATCH 193/295] chore(deps-dev): bump vite from 4.2.3 to 4.5.2 in /extensions/broker-server-api/client-ts (#377) chore(deps-dev): bump vite in /extensions/broker-server-api/client-ts Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.2.3 to 4.5.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- .../client-ts/package-lock.json | 640 +++++++++++++++--- 1 file changed, 564 insertions(+), 76 deletions(-) diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json index cfaa4ad61..b7dbc74c8 100644 --- a/extensions/broker-server-api/client-ts/package-lock.json +++ b/extensions/broker-server-api/client-ts/package-lock.json @@ -276,10 +276,346 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz", - "integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -741,9 +1077,9 @@ } }, "node_modules/esbuild": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", - "integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, "bin": { @@ -753,28 +1089,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.15", - "@esbuild/android-arm64": "0.17.15", - "@esbuild/android-x64": "0.17.15", - "@esbuild/darwin-arm64": "0.17.15", - "@esbuild/darwin-x64": "0.17.15", - "@esbuild/freebsd-arm64": "0.17.15", - "@esbuild/freebsd-x64": "0.17.15", - "@esbuild/linux-arm": "0.17.15", - "@esbuild/linux-arm64": "0.17.15", - "@esbuild/linux-ia32": "0.17.15", - "@esbuild/linux-loong64": "0.17.15", - "@esbuild/linux-mips64el": "0.17.15", - "@esbuild/linux-ppc64": "0.17.15", - "@esbuild/linux-riscv64": "0.17.15", - "@esbuild/linux-s390x": "0.17.15", - "@esbuild/linux-x64": "0.17.15", - "@esbuild/netbsd-x64": "0.17.15", - "@esbuild/openbsd-x64": "0.17.15", - "@esbuild/sunos-x64": "0.17.15", - "@esbuild/win32-arm64": "0.17.15", - "@esbuild/win32-ia32": "0.17.15", - "@esbuild/win32-x64": "0.17.15" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/escape-string-regexp": { @@ -1270,9 +1606,9 @@ } }, "node_modules/rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1464,15 +1800,14 @@ } }, "node_modules/vite": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz", - "integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -1480,12 +1815,16 @@ "engines": { "node": "^14.18.0 || >=16.0.0" }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -1498,6 +1837,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -1772,10 +2114,157 @@ "to-fast-properties": "^2.0.0" } }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, "@esbuild/win32-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz", - "integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "dev": true, "optional": true }, @@ -2142,33 +2631,33 @@ } }, "esbuild": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", - "integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "requires": { - "@esbuild/android-arm": "0.17.15", - "@esbuild/android-arm64": "0.17.15", - "@esbuild/android-x64": "0.17.15", - "@esbuild/darwin-arm64": "0.17.15", - "@esbuild/darwin-x64": "0.17.15", - "@esbuild/freebsd-arm64": "0.17.15", - "@esbuild/freebsd-x64": "0.17.15", - "@esbuild/linux-arm": "0.17.15", - "@esbuild/linux-arm64": "0.17.15", - "@esbuild/linux-ia32": "0.17.15", - "@esbuild/linux-loong64": "0.17.15", - "@esbuild/linux-mips64el": "0.17.15", - "@esbuild/linux-ppc64": "0.17.15", - "@esbuild/linux-riscv64": "0.17.15", - "@esbuild/linux-s390x": "0.17.15", - "@esbuild/linux-x64": "0.17.15", - "@esbuild/netbsd-x64": "0.17.15", - "@esbuild/openbsd-x64": "0.17.15", - "@esbuild/sunos-x64": "0.17.15", - "@esbuild/win32-arm64": "0.17.15", - "@esbuild/win32-ia32": "0.17.15", - "@esbuild/win32-x64": "0.17.15" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "escape-string-regexp": { @@ -2518,9 +3007,9 @@ "dev": true }, "rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -2642,16 +3131,15 @@ "dev": true }, "vite": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz", - "integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "requires": { - "esbuild": "^0.17.5", + "esbuild": "^0.18.10", "fsevents": "~2.3.2", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "postcss": "^8.4.27", + "rollup": "^3.27.1" } }, "vite-plugin-dts": { From 9ef42592f9b9c597d548b4d2fdced3d5f522a54d Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:27:56 +0100 Subject: [PATCH 194/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 26 +++++----- .github/ISSUE_TEMPLATE/documentation.md | 30 ++++++++++++ .github/ISSUE_TEMPLATE/epic_template.md | 41 ++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 33 +++++++++++++ .github/ISSUE_TEMPLATE/process.md | 24 ++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 47 +++++++++++++++---- .../workflows/add_pullrequest_to_project.yml | 1 + 7 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/epic_template.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/process.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6fef046ca..55705dfff 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,37 +2,33 @@ name: Bug Report about: Create a report to help us improve title: "" -labels: ["kind/bug", "scope/mds"] +labels: "kind/bug" assignees: "" --- # Bug Report ## Description - -_A clear and concise description of the bug._ -_If applicable, add screenshots or other information to help explain your problem._ + + ### Expected Behavior - -_A clear and concise description of what you expected to happen._ + ### Observed Behavior - -_A clear and concise description of what happened instead._ + ## Steps to Reproduce - -Steps to reproduce the behavior: + ## Context Information - -_Add any other context about the problem here._ + ## Possible Implementation and Work Breakdown + ```[tasklist] -- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata -- [ ] Refine a Solution Proposal / Work Breakdown +- [ ] adjust the labels, sprint and other metadata +- [ ] refine a solution proposal / work breakdown ``` diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 000000000..4ca8c166d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,30 @@ +--- +name: Documentation Update Request +about: Create a report to help us improve our documentation +title: "" +labels: "task/documentation" +assignees: "" +--- + +# Documentation Update Request + +## Description + + +## Current Documentation + + +## Proposed Changes + + +## Justification + + +## Additional Context + + +## Deadline + + +## Notes + diff --git a/.github/ISSUE_TEMPLATE/epic_template.md b/.github/ISSUE_TEMPLATE/epic_template.md new file mode 100644 index 000000000..24edb0b59 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic_template.md @@ -0,0 +1,41 @@ +--- +name: Epic +about: Help us with new ideas +title: "" +labels: "kind/epic" +assignees: "" +--- + +# Epic + +## Description + + +### Requirements + + +## Work Breakdown + + +```[tasklist] +### Stories +- [ ] Create Stories which can be converted into issues +``` + +## Initiative / goal + + +### Hypothesis + + +## Acceptance criteria and must have scope + + +## Stakeholders + + +## Timeline + + +## Need for refinement + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..2c9a8820f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,33 @@ +--- +name: Feature Request +about: Help us with new features +title: "" +labels: "kind/enhancement" +assignees: "" +--- + +# Feature Request + +## Description + +- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. + +## Which Areas Would Be Affected? + + +## Why Is the Feature Desired? + + +## How does this tie into our current product? + + +## Stakeholders + + +## Solution Proposal and Work Breakdown + + +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine a Solution Proposal / Work Breakdown +``` diff --git a/.github/ISSUE_TEMPLATE/process.md b/.github/ISSUE_TEMPLATE/process.md new file mode 100644 index 000000000..4957c23cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/process.md @@ -0,0 +1,24 @@ +--- +name: Refine Process Request +about: Existing processes must be adapted or new ones created +title: "" +labels: "task/documentation" +assignees: "" +--- + +# Process Refinement Request + +## Description + + +## Current State + + +## Proposed Changes + + +## Related Issues or PRs + + +## Additional Information + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d2c9b82d6..fb93cb530 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,43 @@ +# Pull Request -_What issues does this PR close?_ +_Briefly describe WHAT your PR changes, which features it adds/modifies._ +## How Has This Been Tested? -```[tasklist] -### Checklist -- [ ] The PR title is short and expressive. -- [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/edc-broker-server-extension/tree/main/docs/dev/changelog_updates.md) for more information. -- [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. -- [ ] I have performed a **self-review** -``` +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration +- Test A +- Test B +- ... + +**Test Configuration**: + +- Firmware version: +- Hardware: +- Toolchain: +- SDK: + +## Linked Issue(s) + +_Use keywords to automate: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword_ + +- fixes # (issue) +- closes # (issue) +- ... + +## PR is blocked by + +- [ ] blocked by # (issue) + +# Checklist + +- [ ] I have **formatted the title** correctly and precisely +- [ ] My code follows the **style guidelines** of this project +- [ ] I have performed a **self-review** of my own code +- [ ] I have **commented** my code, particularly in hard-to-understand areas and public classes/methods +- [ ] I have made corresponding changes to the **documentation** +- [ ] My changes generate **no new warnings** (performed checkstyle check locally) +- [ ] I have added **tests that prove my fix** is effective or that my feature works +- [ ] New and existing unit **tests pass locally** with my changes +- [ ] Any dependent **changes have been merged** and published in downstream modules +- [ ] I have added/updated **copyright headers** diff --git a/.github/workflows/add_pullrequest_to_project.yml b/.github/workflows/add_pullrequest_to_project.yml index 81a5a26f1..970b81b39 100644 --- a/.github/workflows/add_pullrequest_to_project.yml +++ b/.github/workflows/add_pullrequest_to_project.yml @@ -5,6 +5,7 @@ on: jobs: add_pullrequest_to_project: + if: github.actor != 'dependabot' && github.actor != 'sovitybot' # ignore PRs from bots name: add_pullrequest_to_project runs-on: ubuntu-latest steps: From abd4ff474f33d7ef51f10529466f0c35e40b70af Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Thu, 22 Feb 2024 07:55:20 +0100 Subject: [PATCH 195/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 49 +++---------------- .github/workflows/add_issue_to_project.yml | 2 +- .../workflows/add_pullrequest_to_project.yml | 2 +- 3 files changed, 10 insertions(+), 43 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb93cb530..c777e39aa 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,43 +1,10 @@ -# Pull Request +_What issues does this PR close?_ -_Briefly describe WHAT your PR changes, which features it adds/modifies._ -## How Has This Been Tested? - -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - -- Test A -- Test B -- ... - -**Test Configuration**: - -- Firmware version: -- Hardware: -- Toolchain: -- SDK: - -## Linked Issue(s) - -_Use keywords to automate: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword_ - -- fixes # (issue) -- closes # (issue) -- ... - -## PR is blocked by - -- [ ] blocked by # (issue) - -# Checklist - -- [ ] I have **formatted the title** correctly and precisely -- [ ] My code follows the **style guidelines** of this project -- [ ] I have performed a **self-review** of my own code -- [ ] I have **commented** my code, particularly in hard-to-understand areas and public classes/methods -- [ ] I have made corresponding changes to the **documentation** -- [ ] My changes generate **no new warnings** (performed checkstyle check locally) -- [ ] I have added **tests that prove my fix** is effective or that my feature works -- [ ] New and existing unit **tests pass locally** with my changes -- [ ] Any dependent **changes have been merged** and published in downstream modules -- [ ] I have added/updated **copyright headers** +```[tasklist] +### Checklist +- [ ] The PR title is short and expressive. +- [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/authority-portal/tree/main/docs/dev/changelog_updates.md) for more information. +- [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. +- [ ] I have performed a **self-review** +``` diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index 60d0da4ba..bbecd2d2a 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -12,5 +12,5 @@ jobs: steps: - uses: actions/add-to-project@v0.5.0 with: - project-url: https://github.com/orgs/sovity/projects/21 + project-url: https://github.com/orgs/sovity/projects/9 github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} diff --git a/.github/workflows/add_pullrequest_to_project.yml b/.github/workflows/add_pullrequest_to_project.yml index 970b81b39..08fc53826 100644 --- a/.github/workflows/add_pullrequest_to_project.yml +++ b/.github/workflows/add_pullrequest_to_project.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/add-to-project@v0.5.0 with: - project-url: https://github.com/orgs/sovity/projects/21 + project-url: https://github.com/orgs/sovity/projects/9 github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} labeled: area/dependency label-operator: NOT From fea1ba0c715cdd62d4fbc6b06d9dfa1799601407 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:22:36 +0100 Subject: [PATCH 196/295] Delete .github/workflows/add_pullrequest_to_project.yml Decision was done in PMO-Software. --- .../workflows/add_pullrequest_to_project.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/add_pullrequest_to_project.yml diff --git a/.github/workflows/add_pullrequest_to_project.yml b/.github/workflows/add_pullrequest_to_project.yml deleted file mode 100644 index 08fc53826..000000000 --- a/.github/workflows/add_pullrequest_to_project.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Add pull request to project action - -on: - pull_request: - -jobs: - add_pullrequest_to_project: - if: github.actor != 'dependabot' && github.actor != 'sovitybot' # ignore PRs from bots - name: add_pullrequest_to_project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/sovity/projects/9 - github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} - labeled: area/dependency - label-operator: NOT From 769846b16db459574c07be97bc316d540f0346b5 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 22 Feb 2024 16:10:52 +0100 Subject: [PATCH 197/295] feat: Use stable contract offer ID (#421) Co-authored-by: Christophe Loiseau --- .env | 2 +- CHANGELOG.md | 2 +- gradle.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env b/.env index 4d1028189..a7da3c174 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.3.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.1 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 045072881..15fe43741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor - Authority Portal API: Added endpoint for receiving all data offers of registered connectors - #### Patch +- Updated dependency version to have stable Policy (and Contract) identifiers. ### Deployment Migration Notes diff --git a/gradle.properties b/gradle.properties index ab3747ec3..2fd5ded26 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.2.0 +sovityEdcExtensionsVersion=7.2.1 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 298e3dff4edfaf05a5e647b3dd1d26431cef22e5 Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:51:18 +0100 Subject: [PATCH 198/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 48 -------------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 62 ++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + 3 files changed, 63 insertions(+), 48 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 55705dfff..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -title: "" -labels: "kind/bug" -assignees: "" ---- - -# Bug Report - -## Description - - - -### Expected Behavior - - -### Observed Behavior - - -## Steps to Reproduce - - -## Context Information - - -## Possible Implementation and Work Breakdown - - -```[tasklist] -- [ ] adjust the labels, sprint and other metadata -- [ ] refine a solution proposal / work breakdown -``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..ac0ab99fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,62 @@ +name: Bug Report Template +description: Create a report to help us improve +labels: ["kind/bug"] +body: + - type: textarea + id: description + attributes: + label: Description - What happened? * + description: A clear and concise description of the bug. + placeholder: Tell us what you see! + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior * + description: A clear and concise description of what you expected to happen. + placeholder: Tell us what you expected! + validations: + required: true + - type: textarea + id: observed + attributes: + label: Observed Behavior * + description: A clear and concise description of what happened instead. + placeholder: Tell us what you observed! + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior. + placeholder: Tell us how to reproduce the issue! + validations: + required: false + - type: textarea + id: context + attributes: + label: Context Information + description: Add any other context about the problem here. + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots or other information to help explain your problem. + validations: + required: false + - type: markdown + attributes: + value: | + _* These fields are mandatory, without filling them it is not possible to create the issue._ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..0086358db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true From 839838a1b66dbfd62ad096c0685e7dd0e4d2e91a Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:54:34 +0100 Subject: [PATCH 199/295] chore: prepare release v3.4.0 (#433) Co-authored-by: Jan Ridderbusch --- .env | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.env b/.env index a7da3c174..b1facc7c8 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.3.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.4.0 EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.1 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 15fe43741..e03d3d2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,15 +14,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major #### Minor + +#### Patch + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION}}` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` +- Sovity EDC CE: {{ CE Release Link }} + +## [v3.4.0] - 2024-02-27 + +### Overview + +Release to accommodate the Authority Portal release. + +### Detailed Changes + +#### Minor + - Authority Portal API: Added endpoint for receiving all data offers of registered connectors #### Patch + - Updated dependency version to have stable Policy (and Contract) identifiers. ### Deployment Migration Notes +_No special deployment migration steps required_ + #### Compatible Versions +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.4.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` +- Sovity EDC CE: [`7.2.1`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.1) + ## [v3.3.0] - 2024-02-14 ### Overview From be45136a62466f9dd72b2bce9f8c7b0e406c9a5d Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:05:47 +0100 Subject: [PATCH 200/295] ci: enable CodeQL (#434) --- .github/workflows/codeql.yml | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..3c6b0894f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,49 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '34 8 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java-kotlin', 'javascript-typescript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From d1f4c65319d85a1d81ee4fc434f0c380ca7dcd58 Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:23:01 +0100 Subject: [PATCH 201/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature_request.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2c9a8820f..5ff7afa21 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -30,4 +30,5 @@ assignees: "" ```[tasklist] - [ ] Fix the GitHub Projects Labels, Sprint and other Metadata - [ ] Refine a Solution Proposal / Work Breakdown +- [ ] (For Tech Team): Include acceptance criteria for the sub-tasks of the work breakdown ``` From e21c27f17ecfbe9796b57543cff2dd256b7c7cab Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:30:41 +0100 Subject: [PATCH 202/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ac0ab99fe..93c91f3fd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,5 +1,5 @@ name: Bug Report Template -description: Create a report to help us improve +description: Report a bug to help us improve labels: ["kind/bug"] body: - type: textarea From 0740d05bb3306aa4bae3a89bb35c8b5cdb6e3678 Mon Sep 17 00:00:00 2001 From: kulgg <75735874+kulgg@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:19:12 +0100 Subject: [PATCH 203/295] chore: prepare release (#440) --- .env | 4 ++-- CHANGELOG.md | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.env b/.env index b1facc7c8..c9cd83d7c 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.4.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.5.0 EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.1 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.4.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.5.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e03d3d2c8..ae6892a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,28 @@ _No special deployment migration steps required_ - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v3.5.0] - 2024-02-29 + +### Overview + +Enable better integration of Broker UI and Authority Portal + +### Detailed Changes + +#### Minor + +- Added query params for the connector endpoints filter + +#### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.5.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.5.0` +- Sovity EDC CE: [`7.2.1`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.1) + ## [v3.4.0] - 2024-02-27 ### Overview From cacd6fc83e1fd504abb3788fd9dba49cbe94c1b0 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:44:09 +0100 Subject: [PATCH 204/295] chore: Remove deprecated endpoint (#441) Co-authored-by: Jan Ridderbusch --- CHANGELOG.md | 5 ++- .../api/BrokerServerResource.java | 10 ----- .../api/model/DataOfferCountResult.java | 21 ---------- .../BrokerServerResourceImpl.java | 6 --- ...rityPortalConnectorMetadataApiService.java | 18 --------- ...thorityPortalConnectorMetadataApiTest.java | 38 ------------------- 6 files changed, 4 insertions(+), 94 deletions(-) delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ae6892a9f..999d73e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,13 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major +- Authority Portal API: Removed deprecated data offer count endpoint + #### Minor #### Patch ### Deployment Migration Notes -_No special deployment migration steps required_ +- Authority Portal API: The deprecated data offer count endpoint was removed: ~~``authority-portal-api/data-offer-counts``~~. + Alternatively the connector metadata endpoint should be used: `authority-portal-api/connectors`. #### Compatible Versions diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index f3228ec40..c9a780024 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -23,7 +23,6 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import io.swagger.v3.oas.annotations.Operation; @@ -90,15 +89,6 @@ public interface BrokerServerResource { @Operation(description = "Provide Connector metadata by provided Connector Endpoints") List getConnectorMetadata(List endpoints, @QueryParam("adminApiKey") String adminApiKey); - @POST - @Deprecated - @Path("authority-portal-api/data-offer-counts") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query the amount of public Data Offers by provided Connector URLs." + - "This endpoint has been replaced by the Authority Portal Connector Metadata endpoint and will be removed in the near future.") - DataOfferCountResult dataOfferCount(List endpoints); - @POST @Path("authority-portal-api/organization-metadata") @Consumes(MediaType.APPLICATION_JSON) diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java deleted file mode 100644 index 061644ef3..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferCountResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.Map; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Number of Data Offers per Connector endpoint.", requiredMode = Schema.RequiredMode.REQUIRED) -public class DataOfferCountResult { - @Schema(description = "Map from endpoint URL to Data Offer count", requiredMode = Schema.RequiredMode.REQUIRED) - private Map dataOfferCount; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 02d57edde..3d4b7f28c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -24,7 +24,6 @@ import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; import de.sovity.edc.ext.brokerserver.db.DslContextFactory; @@ -96,11 +95,6 @@ public List getConnectorMetadata(List endp return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.getMetadataByEndpoints(dsl, endpoints)); } - @Override - public DataOfferCountResult dataOfferCount(List endpoints) { - return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.countByEndpoints(dsl, endpoints)); - } - @Override public void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest organizationMetadataRequest, String adminApiKey) { adminApiKeyValidator.validateAdminApiKey(adminApiKey); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java index 68d726f14..60d103bef 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java @@ -15,17 +15,10 @@ package de.sovity.edc.ext.brokerserver.services.api; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferCountResult; -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; -import org.jooq.impl.DSL; import java.util.List; -import java.util.function.Function; - -import static java.util.stream.Collectors.toMap; @RequiredArgsConstructor public class AuthorityPortalConnectorMetadataApiService { @@ -44,15 +37,4 @@ public List getMetadataByEndpoints(DSLContext dsl, )) .toList(); } - - public DataOfferCountResult countByEndpoints(DSLContext dsl, List endpoints) { - var connectorMetadata = getMetadataByEndpoints(dsl, endpoints); - - var numDataOffers = connectorMetadata.stream().distinct().collect(toMap( - AuthorityPortalConnectorInfo::getConnectorEndpoint, - AuthorityPortalConnectorInfo::getDataOfferCount - )); - - return new DataOfferCountResult(numDataOffers); - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java index 51eb4f195..f8c230ab6 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java @@ -100,44 +100,6 @@ void testConnectorMetadataByEndpoints() { }); } - @Test - void testCountByEndpoints() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var now = OffsetDateTime.now().withNano(0); - - createConnector(dsl, now, 1); - createDataOffer(dsl, now, 1, 1); - createDataOffer(dsl, now, 1, 2); - - createConnector(dsl, now, 2); - createDataOffer(dsl, now, 2, 1); - - createConnector(dsl, now, 3); - createDataOffer(dsl, now, 3, 1); - - createConnector(dsl, now, 4); - - // act - var actual = brokerServerClient().brokerServerApi().dataOfferCount( - Arrays.asList( - getEndpoint(1), - getEndpoint(1), // having this twice should not crash the query - getEndpoint(2), - getEndpoint(4), - getEndpoint(5) // having this not existing should not crash the query - )); - - // assert - var dataOfferCountMap = actual.getDataOfferCount(); - assertThat(dataOfferCountMap).isEqualTo(Map.of( - getEndpoint(1), 2, - getEndpoint(2), 1, - getEndpoint(4), 0 - )); - }); - } - private AuthorityPortalConnectorInfo forConnector(List actual, int iConnector) { return actual.stream() .filter(connectorMetadata -> From 158ed01012d2b060ddae0f2fdc7f928f9c42ee57 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:21:07 +0100 Subject: [PATCH 205/295] chore: disable dependabot version updates --- .github/dependabot.yml | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 0224f6451..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: 2 -updates: - # GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - target-branch: "main" - open-pull-requests-limit: 50 - labels: - - "area/dependency" - - "area/github" - # Docker - - package-ecosystem: "docker" - directory: "/connector/" - schedule: - interval: "daily" - target-branch: "main" - open-pull-requests-limit: 50 - labels: - - "area/dependency" - - "area/docker" - # Gradle - - package-ecosystem: "gradle" - directory: "/" - schedule: - interval: "daily" - target-branch: "main" - open-pull-requests-limit: 50 - labels: - - "area/dependency" - - "area/java" - # NPM - - package-ecosystem: "npm" - directory: "/extensions/broker-server-api/client-ts/" - schedule: - interval: "daily" - target-branch: "main" - open-pull-requests-limit: 50 - labels: - - "area/dependency" - - "area/javascript" From 8c48bdb6dc87f65c01c254d4dca6d18f64c72936 Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:30:01 +0100 Subject: [PATCH 206/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/add_issue_to_project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index bbecd2d2a..796f755df 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -10,7 +10,7 @@ jobs: name: add_issue_to_project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.5.0 + - uses: actions/add-to-project@v0.6.0 with: project-url: https://github.com/orgs/sovity/projects/9 github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} From 45a69c59d643b13bcf5fd62cd9875c9b3ff372bd Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:26:19 +0100 Subject: [PATCH 207/295] feat: Add MDS ID filter and new "addConnectorsWithMdsId" endpoint (#459) --- CHANGELOG.md | 2 + .../api/BrokerServerResource.java | 7 ++ .../api/model/AddedConnector.java | 34 +++++++ .../api/model/ConnectorCreationRequest.java | 34 +++++++ .../BrokerServerExtensionContextBuilder.java | 3 +- .../BrokerServerResourceImpl.java | 7 ++ .../services/api/ConnectorApiService.java | 36 +++++++ .../api/filtering/CatalogFilterService.java | 6 ++ .../ext/brokerserver/utils/MdsIdUtils.java | 4 + .../services/api/AddConnectorsApiTest.java | 93 +++++++++++++++++-- .../services/api/CatalogApiTest.java | 33 +++++++ 11 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java create mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 999d73e50..65e00ec08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Minor +- API: Added endpoint for adding connectors and associated MDS IDs + #### Patch ### Deployment Migration Notes diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java index c9a780024..5bcbfca82 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.brokerserver.api; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorCreationRequest; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; @@ -76,6 +77,12 @@ public interface BrokerServerResource { @Operation(description = "Add unknown Connectors to the Broker Server") void addConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); + @PUT + @Path("connectors-with-mdsid") + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Add unknown Connectors with MDS IDs to the Broker Server") + void addConnectorsWithMdsIds(ConnectorCreationRequest connectors, @QueryParam("adminApiKey") String adminApiKey); + @DELETE @Path("connectors") @Consumes(MediaType.APPLICATION_JSON) diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java new file mode 100644 index 000000000..2a71d0e5e --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Information for adding an unknown connector.", requiredMode = Schema.RequiredMode.REQUIRED) +public class AddedConnector { + @Schema(description = "Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + @Schema(description = "Organization MDS ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String mdsId; +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java new file mode 100644 index 000000000..a638df809 --- /dev/null +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.brokerserver.api.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Wrapper for adding unknown Connectors with MDS IDs.", requiredMode = Schema.RequiredMode.REQUIRED) +public class ConnectorCreationRequest { + @Schema(description = "Connectors", requiredMode = Schema.RequiredMode.REQUIRED) + private List connectors; +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 6f4b43084..44e7290d2 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -285,7 +285,8 @@ public static BrokerServerExtensionContext buildContext( ); var connectorApiService = new ConnectorApiService( connectorService, - brokerEventLogger + brokerEventLogger, + connectorQueries ); var dataOfferDetailApiService = new DataOfferDetailApiService( dataOfferDetailPageQueryService, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java index 3d4b7f28c..7f38ea0d1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorCreationRequest; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; @@ -83,6 +84,12 @@ public void addConnectors(List endpoints, String adminApiKey) { dslContextFactory.transaction(dsl -> connectorApiService.addConnectors(dsl, endpoints)); } + @Override + public void addConnectorsWithMdsIds(ConnectorCreationRequest connectors, String adminApiKey) { + adminApiKeyValidator.validateAdminApiKey(adminApiKey); + dslContextFactory.transaction(dsl -> connectorApiService.addConnectorsWithMdsIds(dsl, connectors)); + } + @Override public void deleteConnectors(List endpoints, String adminApiKey) { adminApiKeyValidator.validateAdminApiKey(adminApiKey); diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java index 46e017be9..ffa3c9c5e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java @@ -14,7 +14,11 @@ package de.sovity.edc.ext.brokerserver.services.api; +import de.sovity.edc.ext.brokerserver.api.model.ConnectorCreationRequest; +import de.sovity.edc.ext.brokerserver.api.model.AddedConnector; +import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.brokerserver.utils.MdsIdUtils; import de.sovity.edc.ext.brokerserver.utils.UrlUtils; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -29,6 +33,8 @@ public class ConnectorApiService { private final ConnectorService connectorService; private final BrokerEventLogger brokerEventLogger; + private final ConnectorQueries connectorQueries; + public void addConnectors(DSLContext dsl, List connectorEndpoints) { var existingEndpoints = connectorService.getConnectorEndpoints(dsl); @@ -41,8 +47,38 @@ public void addConnectors(DSLContext dsl, List connectorEndpoints) { connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); } + public void addConnectorsWithMdsIds(DSLContext dsl, ConnectorCreationRequest connectorCreationRequests) { + var connectors = connectorCreationRequests.getConnectors(); + var existingEndpoints = connectorService.getConnectorEndpoints(dsl); + + connectors.removeIf(it -> it.getConnectorEndpoint() == null || it.getMdsId() == null); + connectors.forEach(it -> { + it.setConnectorEndpoint(it.getConnectorEndpoint().trim()); + it.setMdsId(it.getMdsId().trim()); + }); + connectors.removeIf(it -> + !UrlUtils.isValidUrl(it.getConnectorEndpoint()) + || !MdsIdUtils.isValidMdsId(it.getMdsId()) + || existingEndpoints.contains(it.getConnectorEndpoint()) + ); + + var endpoints = connectors.stream().map(AddedConnector::getConnectorEndpoint).collect(toSet()); + connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); + addMdsIdsToConnectors(dsl, connectors); + } + public void deleteConnectors(DSLContext dsl, List connectorEndpoints) { connectorService.deleteConnectors(dsl, connectorEndpoints); brokerEventLogger.logConnectorsDeleted(dsl, connectorEndpoints); } + + private void addMdsIdsToConnectors(DSLContext dsl, List connectors) { + connectors.forEach(it -> { + var connector = connectorQueries.findByEndpoint(dsl, it.getConnectorEndpoint()); + if (connector != null) { + connector.setMdsId(it.getMdsId()); + connector.update(); + } + }); + } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java index 19448368d..860a796ab 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; +import org.jooq.impl.DSL; import java.util.Comparator; import java.util.List; @@ -88,6 +89,11 @@ private List getAvailableFilters() { "curatorOrganizationName", "Organization Name" ), + catalogFilterAttributeDefinitionService.forField( + fields -> DSL.coalesce(fields.getConnectorTable().MDS_ID, "Unknown"), + "curatorMdsId", + "MDS ID" + ), catalogFilterAttributeDefinitionService.buildConnectorEndpointFilter() ); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java index 8bc3fb182..a832e20b6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java @@ -26,4 +26,8 @@ public static String getMdsIdFromParticipantId(String participantId) { return participantId.split("\\.")[0]; } + + public static Boolean isValidMdsId(String mdsId) { + return mdsId != null && mdsId.matches("^MDSL[A-Za-z0-9]+"); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java index bf01f089b..058882217 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java @@ -16,6 +16,8 @@ import de.sovity.edc.ext.brokerserver.TestUtils; import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; +import de.sovity.edc.ext.brokerserver.client.gen.model.AddedConnector; +import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorCreationRequest; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; import de.sovity.edc.ext.brokerserver.db.TestDatabase; @@ -56,27 +58,100 @@ void testAddConnectors() { client.brokerServerApi().addConnectors(ADMIN_API_KEY, List.of()); client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( + null, + "", + " ", + "\t", + "http://a", + "http://b" + )); + + assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) + .extracting(ConnectorListEntry::getEndpoint) + .containsExactlyInAnyOrder("http://a", "http://b"); + + client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( + "http://b", + " http://b\r\n", + "http://c" + )); + + assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) + .extracting(ConnectorListEntry::getEndpoint) + .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); + }); + } + + @Test + void testAddConnectorsWithMdsIds() { + TEST_DATABASE.testTransaction(dsl -> { + client.brokerServerApi().addConnectorsWithMdsIds(ADMIN_API_KEY, new ConnectorCreationRequest(List.of())); + + client.brokerServerApi().addConnectorsWithMdsIds(ADMIN_API_KEY, new ConnectorCreationRequest(Arrays.asList( + new AddedConnector( null, + "MDSL1234" + ), + new AddedConnector( "", + "MDSL1234" + ), + new AddedConnector( " ", + "MDSL1234" + ), + new AddedConnector( "\t", + "MDSL1234" + ), + new AddedConnector( "http://a", - "http://b" - )); + "MDSL1234" + ), + new AddedConnector( + " http://b\r\n", + "MDSL1234" + ), + new AddedConnector( + "http://c", + null + ), + new AddedConnector( + "http://d", + "" + ), + new AddedConnector( + "http://e", + " " + ), + new AddedConnector( + "http://f", + "\t" + ) + ))); assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactlyInAnyOrder("http://a", "http://b"); + .extracting(ConnectorListEntry::getEndpoint) + .containsExactlyInAnyOrder("http://a", "http://b"); - client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( + client.brokerServerApi().addConnectorsWithMdsIds(ADMIN_API_KEY, new ConnectorCreationRequest(Arrays.asList( + new AddedConnector( "http://b", + "MDSL1234" + ), + new AddedConnector( " http://b\r\n", - "http://c" - )); + "MDSL1234" + ), + new AddedConnector( + "http://c", + "MDSL1234" + ) + ))); assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); + .extracting(ConnectorListEntry::getEndpoint) + .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); }); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index dc16852db..d344f27fc 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -257,6 +257,7 @@ void testAvailableFilters_noFilter() { "transportMode", "geoReferenceMethod", "curatorOrganizationName", + "curatorMdsId", "connectorEndpoint" ); @@ -270,6 +271,7 @@ void testAvailableFilters_noFilter() { "Transport Mode", "Geo Reference Method", "Organization Name", + "MDS ID", "Connector" ); @@ -301,6 +303,10 @@ void testAvailableFilters_noFilter() { assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getId).containsExactly("Test Org"); assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("Test Org"); + var curatorMdsId = getAvailableFilter(result, "curatorMdsId"); + assertThat(curatorMdsId.getValues()).extracting(CnfFilterItem::getId).containsExactly("MDSL123456AA"); + assertThat(curatorMdsId.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MDSL123456AA"); + var connectorEndpoint = getAvailableFilter(result, "connectorEndpoint"); assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getId).containsExactly("https://my-connector/api/dsp"); assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("https://my-connector/api/dsp"); @@ -516,6 +522,33 @@ void testSearchForOrgName() { }); } + @Test + void testFilterByMdsId() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var today = OffsetDateTime.now().withNano(0); + + var endpoint1 = "https://my-connector-1/api/dsp"; + createConnector(dsl, today, endpoint1, "MDSL1111AA"); + createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); + + var endpoint2 = "https://my-connector-2/api/dsp"; + createConnector(dsl, today, endpoint2, "MDSL2222BB"); + createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); + + // act + var query = new CatalogPageQuery(); + query.setFilter(new CnfFilterValue(List.of( + new CnfFilterValueAttribute("curatorMdsId", List.of("MDSL1111AA")) + ))); + + var actual = brokerServerClient().brokerServerApi().catalogPage(query); + + // assert + assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint1); + }); + } + @Test void testFilterByUnknown() { TEST_DATABASE.testTransaction(dsl -> { From 5889a80893c057f6973a767249e65a04dfb2c410 Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:54:42 +0100 Subject: [PATCH 208/295] chore: prepare release (#461) --- .env | 6 +++--- CHANGELOG.md | 30 +++++++++++++++++++++++++----- gradle.properties | 2 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.env b/.env index c9cd83d7c..6ed521279 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:3.5.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.1 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:2.5.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.0.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.2 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e00ec08..313526a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,14 +13,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Major +#### Minor + +#### Patch + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION }}` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` +- Sovity EDC CE: {{ CE Release Link }} + +## [v4.0.0] - 2024-03-22 + +### Overview + +Release with adjustmets for the ongoing integration with the Authority Portal + +### Detailed Changes + +#### Major + - Authority Portal API: Removed deprecated data offer count endpoint #### Minor - API: Added endpoint for adding connectors and associated MDS IDs -#### Patch - ### Deployment Migration Notes - Authority Portal API: The deprecated data offer count endpoint was removed: ~~``authority-portal-api/data-offer-counts``~~. @@ -28,9 +48,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Compatible Versions -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION}}` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` -- Sovity EDC CE: {{ CE Release Link }} +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.0.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.0.0` +- Sovity EDC CE: [`7.2.2`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.2) ## [v3.5.0] - 2024-02-29 diff --git a/gradle.properties b/gradle.properties index 2fd5ded26..1d1dd54a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.2.1 +sovityEdcExtensionsVersion=7.2.2 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 81a4465baaf1ee4f4a3bd468ab732a58a545fcf6 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:58:19 +0200 Subject: [PATCH 209/295] chore: Prepare release (#463) --- .env | 4 ++-- CHANGELOG.md | 21 +++++++++++++++++++ .../refreshing/ConnectorUpdaterTest.java | 20 +++++++++--------- gradle.properties | 2 +- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.env b/.env index 6ed521279..7bd89c12c 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.0.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.2.2 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.1.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.3.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 313526a9c..a7f8c0f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v4.1.0] - 2024-04-02 + +### Overview + +Pull changes from EDC CE 7.3.0 into the broker. + +### Detailed Changes + +#### Minor + +- Bumped EDC version to 7.3.0: + - Broker UI: Support for UIAsset's `customJsonAsString` and `customJsonLdAsString`, along with their private counterparts. + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.1.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.0.0` +- Sovity EDC CE: [`7.3.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.3.0) + ## [v4.0.0] - 2024-03-22 ### Overview diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index 77f2b960d..797db9607 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -124,12 +124,12 @@ void testConnectorUpdate() { assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isFalse(); assertThat(asset.getHttpDatasourceHintsProxyBody()).isFalse(); - assertThat(asset.getAdditionalProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/a", "x")); - assertThat(dataOffer.getAsset().getAdditionalJsonProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")); - assertThat(dataOffer.getAsset().getPrivateProperties()).isEmpty(); - assertThat(dataOffer.getAsset().getPrivateJsonProperties()).isEmpty(); + assertThat(asset.getCustomJsonAsString()) + .isEqualTo("{\"a\":\"x\"}"); + assertThat(dataOffer.getAsset().getCustomJsonLdAsString()) + .isEqualTo("{\"http://unknown/b\":{\"http://unknown/c\":\"y\"}}"); + assertThat(dataOffer.getAsset().getPrivateCustomJsonAsString()).isNull(); + assertThat(dataOffer.getAsset().getPrivateCustomJsonLdAsString()).isEqualTo("{}"); var policy = contractOffer.getContractPolicy(); assertThat(policy.getConstraints()).hasSize(1); AssertionUtils.assertEqualUsingJson(policy.getConstraints().get(0), createAfterYesterdayConstraint()); @@ -191,10 +191,10 @@ private String createAsset() { Prop.Edc.METHOD, "GET", Prop.Edc.BASE_URL, "http://some.url" )) - .additionalProperties(Map.of("http://unknown/a", "x")) - .additionalJsonProperties(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")) - .privateProperties(Map.of("http://unknown/a-private", "x-private")) - .privateJsonProperties(Map.of("http://unknown/b-private", "{\"http://unknown/c-private\":\"y-private\"}")) + .customJsonAsString("{\"a\":\"x\"}") + .customJsonLdAsString("{\"http://unknown/b\":{\"http://unknown/c\":\"y\"}}") + .privateCustomJsonAsString("{\"a-private\":\"x-private\"}") + .privateCustomJsonLdAsString("{\"http://unknown/b-private\":{\"http://unknown/c-private\":\"y-private\"}}") .build()).getId(); } } diff --git a/gradle.properties b/gradle.properties index 1d1dd54a9..f817db910 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.2.2 +sovityEdcExtensionsVersion=7.3.0 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 483845c8dc3cda300f60f2d0ca881eb3afdff444 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:07:48 +0200 Subject: [PATCH 210/295] chore(deps-dev): bump vite from 4.5.2 to 4.5.3 in /extensions/broker-server-api/client-ts (#465) --- .../broker-server-api/client-ts/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json index b7dbc74c8..0ef861efc 100644 --- a/extensions/broker-server-api/client-ts/package-lock.json +++ b/extensions/broker-server-api/client-ts/package-lock.json @@ -1800,9 +1800,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -3131,9 +3131,9 @@ "dev": true }, "vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "requires": { "esbuild": "^0.18.10", From b47e602d5f0184cb18b095dcb589981fbf9df12f Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:26:28 +0200 Subject: [PATCH 211/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/add_issue_to_project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index 796f755df..02ae5faf8 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -10,7 +10,7 @@ jobs: name: add_issue_to_project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.6.0 + - uses: actions/add-to-project@v1.0.0 with: project-url: https://github.com/orgs/sovity/projects/9 github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} From 044c576874667c01aca08ee03023f1e5832acbe9 Mon Sep 17 00:00:00 2001 From: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:35:22 +0200 Subject: [PATCH 212/295] chore: Prepare release (#468) --- .env | 4 ++-- CHANGELOG.md | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 7bd89c12c..ed5d07e53 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.1.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.1.1 EDC_IMAGE=ghcr.io/sovity/edc-dev:7.3.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f8c0f74..71895a1df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v4.1.1] - 2024-04-11 + +### Overview + +Pull changes from EDC UI 3.1.0 into the broker. + +### Detailed Changes + +#### Patch + +- Bump EDC UI version to 3.1.0 + - "Name" column renamed to "Title" + - Fix status icon for data offers + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.1.1` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.1.0` +- Sovity EDC CE: [`7.3.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.3.0) + ## [v4.1.0] - 2024-04-02 ### Overview From 9eb8e44da10a740fe2d2066f97492fba0dc2c0bf Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Sat, 20 Apr 2024 14:27:23 +0200 Subject: [PATCH 213/295] chore: bump edc-extensions to latest (#470) --- CHANGELOG.md | 22 ++ .../edc/ext/brokerserver/TestAsset.java | 41 +++- .../services/api/CatalogApiTest.java | 209 ++++++++++-------- .../services/api/ConnectorApiTest.java | 24 +- .../services/api/DataOfferDetailApiTest.java | 26 ++- .../offers/DataOfferWriterTestDataHelper.java | 10 +- gradle.properties | 2 +- 7 files changed, 215 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71895a1df..2e6fa9dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` - Sovity EDC CE: {{ CE Release Link }} +## [v4.2.0] - 2024-04-11 + +### Overview + +Bumped EDC CE version to 7.4.2. + +### Detailed Changes + +#### Minor + +- Bumped EDC CE version to 7.4.2. +- Bumped EDC UI version to {{x.y.z}}. + - Better handling of custom properties. + +### Deployment Migration Notes + +#### Compatible Versions + +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{x.y.z}}` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{x.y.z}}` +- Sovity EDC CE: [`7.4.2`](https://github.com/sovity/edc-extensions/releases/tag/v{{x.y.z}}) + ## [v4.1.1] - 2024-04-11 ### Overview diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java index c954d6a86..9dba67370 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java @@ -16,8 +16,13 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.utils.jsonld.vocab.Prop; -import jakarta.json.Json; import jakarta.json.JsonObject; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -26,17 +31,25 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TestAsset { + public static JsonObject getAssetJsonLd(String assetId) { - return getAssetJsonLd(assetId, Map.of()); + return getAssetJsonLd( + UiAssetCreateRequest.builder() + .id(assetId) + .build() + ); } - public static JsonObject getAssetJsonLd(String assetId, Map properties) { - return Json.createObjectBuilder() - .add(Prop.ID, assetId) - .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder() - .add(Prop.Edc.ASSET_ID, assetId) - .addAll(Json.createObjectBuilder(properties))) - .build(); + public static JsonObject getAssetJsonLd(UiAssetCreateRequest request) { + return getUiAssetMapper().buildAssetJsonLd( + request.toBuilder() + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.BASE_URL, "https://example.com" + )) + .build(), + "orgName" + ); } /** @@ -59,4 +72,14 @@ public static void setDataOfferAssetMetadata(DataOfferRecord dataOfferRecord, Js dataOfferRecord.setAssetId(fetchedDataOffer.getAssetId()); dataOfferRecordUpdater.updateDataOffer(dataOfferRecord, fetchedDataOffer, false); } + + public static UiAssetMapper getUiAssetMapper() { + return new UiAssetMapper( + new EdcPropertyUtils(), + new AssetJsonLdUtils(), + new MarkdownToTextConverter(), + new TextUtils(), + "http://own-connector-endpoint"::equals + ); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java index d344f27fc..fb508c39d 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java @@ -31,8 +31,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import jakarta.json.JsonObject; import lombok.SneakyThrows; import org.eclipse.edc.junit.annotations.ApiTest; @@ -67,9 +66,9 @@ class CatalogApiTest { @BeforeEach void setUp(EdcExtension extension) { extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", - BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=https://my-connector2/api/dsp,Example2=https://my-connector3/api/dsp" + BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", + BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", + BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=https://my-connector2/api/dsp,Example2=https://my-connector3/api/dsp" ))); } @@ -79,7 +78,12 @@ void testDataSpace_two_dataspaces_filter_for_one() { // arrange var today = OffsetDateTime.now().withNano(0); - var assetJsonLd = getAssetJsonLd("my-asset", Map.of(Prop.Dcterms.TITLE, "My Asset")); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset") + .title("My Asset") + .build() + ); // Dataspace: MDS createConnector(dsl, today, "https://my-connector/api/dsp"); @@ -91,7 +95,7 @@ void testDataSpace_two_dataspaces_filter_for_one() { var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataSpace", List.of("Example1")) + new CnfFilterValueAttribute("dataSpace", List.of("Example1")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); @@ -108,7 +112,12 @@ void testConnectorEndpointFilter_two_connectors_filter_for_one() { // arrange var today = OffsetDateTime.now().withNano(0); - var assetJsonLd = getAssetJsonLd("my-asset", Map.of(Prop.Dcterms.TITLE, "My Asset")); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset") + .title("My Asset") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); @@ -118,7 +127,7 @@ void testConnectorEndpointFilter_two_connectors_filter_for_one() { var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("connectorEndpoint", List.of("https://my-connector/api/dsp")) + new CnfFilterValueAttribute("connectorEndpoint", List.of("https://my-connector/api/dsp")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); @@ -151,9 +160,9 @@ void test_available_filter_values_to_filter_by() { // assert that the filter values are correct var dataSpace = getAvailableFilter(result, "dataSpace"); assertThat(dataSpace.getValues()).containsExactly( - new CnfFilterItem("Example1", "Example1"), - new CnfFilterItem("Example2", "Example2"), - new CnfFilterItem("MDS", "MDS") + new CnfFilterItem("Example1", "Example1"), + new CnfFilterItem("Example2", "Example2"), + new CnfFilterItem("MDS", "MDS") ); }); } @@ -164,9 +173,12 @@ void testDataOfferDetails() { // arrange var today = OffsetDateTime.now().withNano(0); - var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Dcterms.TITLE, "My Asset" - )); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset") + .title("My Asset") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); @@ -178,8 +190,8 @@ void testDataOfferDetails() { assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("https://my-connector/api/dsp"); assertThat(dataOfferResult.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(CatalogDataOffer.ConnectorOnlineStatusEnum.ONLINE); - assertThat(dataOfferResult.getAssetId()).isEqualTo("my-asset-1"); - assertThat(dataOfferResult.getAsset().getAssetId()).isEqualTo("my-asset-1"); + assertThat(dataOfferResult.getAssetId()).isEqualTo("my-asset"); + assertThat(dataOfferResult.getAsset().getAssetId()).isEqualTo("my-asset"); assertThat(dataOfferResult.getAsset().getTitle()).isEqualTo("My Asset"); assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); }); @@ -213,30 +225,42 @@ void testAvailableFilters_noFilter() { // arrange var today = OffsetDateTime.now().withNano(0); - var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2", - Prop.Mds.DATA_MODEL, "my-data-model", - Prop.Mds.GEO_REFERENCE_METHOD, "my-geo-ref" - )); - - var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "my-transport-mode-2", - Prop.Mds.DATA_SUBCATEGORY, "MY-SUBCATEGORY-2" - )); - - var assetJsonLd3 = getAssetJsonLd("my-asset-3", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "MY-TRANSPORT-MODE-1", - Prop.Mds.DATA_SUBCATEGORY, "my-subcategory-1" - )); - - var assetJsonLd4 = getAssetJsonLd("my-asset-4", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-1", - Prop.Mds.TRANSPORT_MODE, "" - )); + var assetJsonLd1 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-1") + .dataCategory("my-category-1") + .transportMode("MY-TRANSPORT-MODE-1") + .dataSubcategory("MY-SUBCATEGORY-2") + .dataModel("my-data-model") + .geoReferenceMethod("my-geo-ref") + .build() + ); + + var assetJsonLd2 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-2") + .dataCategory("my-category-1") + .transportMode("my-transport-mode-2") + .dataSubcategory("MY-SUBCATEGORY-2") + .build() + ); + + var assetJsonLd3 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-3") + .dataCategory("my-category-1") + .transportMode("MY-TRANSPORT-MODE-1") + .dataSubcategory("my-subcategory-1") + .build() + ); + + var assetJsonLd4 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-4") + .dataCategory("my-category-1") + .transportMode("") + .build() + ); createOrganizationMetadata(dsl, "MDSL123456AA", "Test Org"); createConnector(dsl, today, "https://my-connector/api/dsp", "MDSL123456AA"); @@ -248,32 +272,32 @@ void testAvailableFilters_noFilter() { var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getId) - .containsExactly( - "dataSpace", - "dataCategory", - "dataSubcategory", - "dataModel", - "transportMode", - "geoReferenceMethod", - "curatorOrganizationName", - "curatorMdsId", - "connectorEndpoint" - ); + .extracting(CnfFilterAttribute::getId) + .containsExactly( + "dataSpace", + "dataCategory", + "dataSubcategory", + "dataModel", + "transportMode", + "geoReferenceMethod", + "curatorOrganizationName", + "curatorMdsId", + "connectorEndpoint" + ); assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getTitle) - .containsExactly( - "Data Space", - "Data Category", - "Data Subcategory", - "Data Model", - "Transport Mode", - "Geo Reference Method", - "Organization Name", - "MDS ID", - "Connector" - ); + .extracting(CnfFilterAttribute::getTitle) + .containsExactly( + "Data Space", + "Data Category", + "Data Subcategory", + "Data Model", + "Transport Mode", + "Geo Reference Method", + "Organization Name", + "MDS ID", + "Connector" + ); var dataSpace = getAvailableFilter(result, "dataSpace"); assertThat(dataSpace.getValues()).extracting(CnfFilterItem::getId).containsExactly("MDS"); @@ -325,7 +349,12 @@ void testSearchCaseInsensitive() { // arrange var today = OffsetDateTime.now().withNano(0); - var assetJsonLd = getAssetJsonLd("123", Map.of(Prop.Dcterms.TITLE, "Hello")); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("123") + .title("Hello") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); @@ -343,8 +372,8 @@ void testSearchCaseInsensitive() { private CnfFilterAttribute getAvailableFilter(CatalogPageResult result, String filterId) { return result.getAvailableFilters().getFields().stream() - .filter(it -> it.getId().equals(filterId)).findFirst() - .orElseThrow(() -> new IllegalStateException("Filter not found")); + .filter(it -> it.getId().equals(filterId)).findFirst() + .orElseThrow(() -> new IllegalStateException("Filter not found")); } @Test @@ -353,14 +382,20 @@ void testAvailableFilters_withFilter() { // arrange var today = OffsetDateTime.now().withNano(0); - var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", - Prop.Mds.DATA_SUBCATEGORY, "my-subcategory" - )); + var assetJsonLd1 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-1") + .dataCategory("my-category") + .dataSubcategory("my-subcategory") + .build() + ); - var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mds.DATA_SUBCATEGORY, "my-other-subcategory" - )); + var assetJsonLd2 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-2") + .dataSubcategory("my-other-subcategory") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); @@ -368,7 +403,7 @@ void testAvailableFilters_withFilter() { var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataCategory", List.of("")) + new CnfFilterValueAttribute("dataCategory", List.of("")) ))); var result = brokerServerClient().brokerServerApi().catalogPage(query); @@ -399,7 +434,7 @@ void testPagination_firstPage() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(range(0, 10).mapToObj("my-asset-%d"::formatted).toList()); + .isEqualTo(range(0, 10).mapToObj("my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(1); @@ -428,7 +463,7 @@ void testPagination_secondPage() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(range(10, 15).mapToObj("my-asset-%d"::formatted).toList()); + .isEqualTo(range(10, 15).mapToObj("my-asset-%d"::formatted).toList()); var actual = result.getPaginationMetadata(); assertThat(actual.getPageOneBased()).isEqualTo(2); @@ -459,9 +494,9 @@ void testSortingByPopularity() { var result = brokerServerClient().brokerServerApi().catalogPage(query); assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly( - "asset-2", - "asset-1", - "asset-3" + "asset-2", + "asset-1", + "asset-3" ); }); } @@ -568,7 +603,7 @@ void testFilterByUnknown() { // act var query = new CatalogPageQuery(); query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("curatorOrganizationName", List.of("Unknown")) + new CnfFilterValueAttribute("curatorOrganizationName", List.of("Unknown")) ))); var actual = brokerServerClient().brokerServerApi().catalogPage(query); @@ -650,15 +685,15 @@ private void createOrganizationMetadata(DSLContext dsl, String mdsId, String nam private Policy dummyPolicy() { return Policy.Builder.newInstance() - .type(PolicyType.SET) - .build(); + .type(PolicyType.SET) + .build(); } private DataOfferDetailPageResult dataOfferDetails(String endpoint, String assetId) { var query = DataOfferDetailPageQuery.builder() - .connectorEndpoint(endpoint) - .assetId(assetId) - .build(); + .connectorEndpoint(endpoint) + .assetId(assetId) + .build(); return brokerServerClient().brokerServerApi().dataOfferDetailPage(query); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java index c567728af..fa3419438 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java @@ -26,7 +26,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.utils.jsonld.vocab.Prop; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import jakarta.json.JsonObject; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -62,10 +62,13 @@ void testQueryConnectors() { TEST_DATABASE.testTransaction(dsl -> { var today = OffsetDateTime.now().withNano(0); - var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", - Prop.Dcterms.TITLE, "My Asset 1" - )); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-1") + .title("My Asset 1") + .dataCategory("my-category") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); @@ -88,10 +91,13 @@ void testQueryConnectorDetails() { TEST_DATABASE.testTransaction(dsl -> { var today = OffsetDateTime.now().withNano(0); - var assetJsonLd = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", - Prop.Dcterms.TITLE, "My Asset 1" - )); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-1") + .title("My Asset 1") + .dataCategory("my-category") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createConnector(dsl, today, "https://my-connector2/api/dsp"); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java index 299742ba7..007ac7ffc 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java @@ -22,7 +22,7 @@ import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.utils.jsonld.vocab.Prop; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import jakarta.json.JsonObject; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -62,15 +62,21 @@ void testQueryDataOfferDetails() { TEST_DATABASE.testTransaction(dsl -> { var today = OffsetDateTime.now().withNano(0); - var assetJsonLd1 = getAssetJsonLd("my-asset-1", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category", - Prop.Dcterms.TITLE, "My Asset 1" - )); - - var assetJsonLd2 = getAssetJsonLd("my-asset-2", Map.of( - Prop.Mds.DATA_CATEGORY, "my-category-2", - Prop.Dcterms.TITLE, "My Asset 2" - )); + var assetJsonLd1 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-1") + .title("My Asset 1") + .dataCategory("my-category") + .build() + ); + + var assetJsonLd2 = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id("my-asset-2") + .title("My Asset 2") + .dataCategory("my-category-2") + .build() + ); createConnector(dsl, today, "https://my-connector/api/dsp"); createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java index 312e83765..664ac3b30 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java @@ -20,8 +20,8 @@ import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -30,7 +30,6 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -115,7 +114,12 @@ private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { } public String dummyAssetJson(Do dataOffer) { - var assetJsonLd = getAssetJsonLd(dataOffer.getAssetId(), Map.of(Prop.Dcterms.TITLE, dataOffer.getAssetTitle())); + var assetJsonLd = getAssetJsonLd( + UiAssetCreateRequest.builder() + .id(dataOffer.getAssetId()) + .title(dataOffer.getAssetTitle()) + .build() + ); return JsonUtils.toJson(assetJsonLd); } diff --git a/gradle.properties b/gradle.properties index f817db910..69a547ff1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.3.0 +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From 5211a1b731e5c2ac7ebe0525818902a24bd1a5e3 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Sat, 20 Apr 2024 16:12:41 +0200 Subject: [PATCH 214/295] chore: prepare release (#471) --- .env | 6 +++--- CHANGELOG.md | 12 +++++++----- gradle.properties | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.env b/.env index ed5d07e53..bbe3d5a3c 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.1.1 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.3.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.1.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.2.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.4.2 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e6fa9dab..5660d9036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,16 +36,16 @@ Bumped EDC CE version to 7.4.2. #### Minor - Bumped EDC CE version to 7.4.2. -- Bumped EDC UI version to {{x.y.z}}. - - Better handling of custom properties. +- Bumped EDC UI version to 3.2.2. +- Better handling of custom properties. ### Deployment Migration Notes #### Compatible Versions -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{x.y.z}}` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{x.y.z}}` -- Sovity EDC CE: [`7.4.2`](https://github.com/sovity/edc-extensions/releases/tag/v{{x.y.z}}) +- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.2.0` +- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` +- Sovity EDC CE: [`7.4.2`](https://github.com/sovity/edc-extensions/releases/tag/v7.4.2) ## [v4.1.1] - 2024-04-11 @@ -63,6 +63,8 @@ Pull changes from EDC UI 3.1.0 into the broker. ### Deployment Migration Notes +_No special deployment migration steps required_ + #### Compatible Versions - Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.1.1` diff --git a/gradle.properties b/gradle.properties index 69a547ff1..3d66ee0fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ sovityBrokerServerGroup=de.sovity.broker sovityBrokerServerVersion=0.0.1-SNAPSHOT # Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionsVersion=7.4.2 sovityEdcExtensionGroup=de.sovity.edc.ext sovityEdcGroup=de.sovity.edc From a5544c9eee25e9458b2f7d86b5f9ef0dddaa6c49 Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Thu, 2 May 2024 10:33:57 +0200 Subject: [PATCH 215/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#472)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/add_issue_to_project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index 02ae5faf8..76fd8814c 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -10,7 +10,7 @@ jobs: name: add_issue_to_project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.0 + - uses: actions/add-to-project@v1.0.1 with: project-url: https://github.com/orgs/sovity/projects/9 github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} From bb916d893e967e5beaacd4ce68ffbeef210d9ad7 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 3 May 2024 08:33:34 +0200 Subject: [PATCH 216/295] Clearing the way for the merge with edc-extensions --- .dockerignore => conflicts/.dockerignore | 0 .editorconfig => conflicts/.editorconfig | 0 .env => conflicts/.env | 0 .gitattributes => conflicts/.gitattributes | 0 .../.github}/ISSUE_TEMPLATE/bug_report.yaml | 0 .../.github}/ISSUE_TEMPLATE/config.yml | 0 .../.github}/ISSUE_TEMPLATE/documentation.md | 0 .../.github}/ISSUE_TEMPLATE/enhancement.md | 0 .../.github}/ISSUE_TEMPLATE/epic_template.md | 0 .../.github}/ISSUE_TEMPLATE/feature_request.md | 0 .../ISSUE_TEMPLATE/mds_feature_request.md | 0 .../.github}/ISSUE_TEMPLATE/process.md | 0 .../.github}/ISSUE_TEMPLATE/release.md | 0 .../.github}/PULL_REQUEST_TEMPLATE.md | 0 .../.github}/workflows/add_issue_to_project.yml | 0 .../build-and-publish-connector-images.yml | 0 .../build-and-publish-ts-api-client.yml | 0 .../.github}/workflows/code_analysis.yml | 0 .../.github}/workflows/codeql.yml | 0 .../.github}/workflows/license_scan.yml | 0 .../.github}/workflows/release_docs_zip.yml | 0 .../.github}/workflows/secret_scan.yml | 0 .../.github}/workflows/security_scan.yml | 0 .../.pre-commit-README.md | 0 .../.pre-commit-config.yaml | 0 CHANGELOG.md => conflicts/CHANGELOG.md | 0 .../CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => conflicts/CONTRIBUTING.md | 0 LICENSE.md => conflicts/LICENSE.md | 0 README.md => conflicts/README.md | 0 SECURITY.md => conflicts/SECURITY.md | 0 STYLEGUIDE.md => conflicts/STYLEGUIDE.md | 0 build.gradle.kts => conflicts/build.gradle.kts | 0 .../docker-compose.yaml | 0 .../docs}/dev/changelog_updates.md | 0 .../docs}/dev/checkstyle/checkstyle-config.xml | 0 .../gradle.properties | 0 .../gradle}/wrapper/gradle-wrapper.properties | 0 gradlew => conflicts/gradlew | 0 gradlew.bat => conflicts/gradlew.bat | 0 .../settings.gradle.kts | 0 gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 0 bytes 42 files changed, 0 insertions(+), 0 deletions(-) rename .dockerignore => conflicts/.dockerignore (100%) rename .editorconfig => conflicts/.editorconfig (100%) rename .env => conflicts/.env (100%) rename .gitattributes => conflicts/.gitattributes (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/bug_report.yaml (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/config.yml (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/documentation.md (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/enhancement.md (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/epic_template.md (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/feature_request.md (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/mds_feature_request.md (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/process.md (100%) rename {.github => conflicts/.github}/ISSUE_TEMPLATE/release.md (100%) rename {.github => conflicts/.github}/PULL_REQUEST_TEMPLATE.md (100%) rename {.github => conflicts/.github}/workflows/add_issue_to_project.yml (100%) rename {.github => conflicts/.github}/workflows/build-and-publish-connector-images.yml (100%) rename {.github => conflicts/.github}/workflows/build-and-publish-ts-api-client.yml (100%) rename {.github => conflicts/.github}/workflows/code_analysis.yml (100%) rename {.github => conflicts/.github}/workflows/codeql.yml (100%) rename {.github => conflicts/.github}/workflows/license_scan.yml (100%) rename {.github => conflicts/.github}/workflows/release_docs_zip.yml (100%) rename {.github => conflicts/.github}/workflows/secret_scan.yml (100%) rename {.github => conflicts/.github}/workflows/security_scan.yml (100%) rename .pre-commit-README.md => conflicts/.pre-commit-README.md (100%) rename .pre-commit-config.yaml => conflicts/.pre-commit-config.yaml (100%) rename CHANGELOG.md => conflicts/CHANGELOG.md (100%) rename CODE_OF_CONDUCT.md => conflicts/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => conflicts/CONTRIBUTING.md (100%) rename LICENSE.md => conflicts/LICENSE.md (100%) rename README.md => conflicts/README.md (100%) rename SECURITY.md => conflicts/SECURITY.md (100%) rename STYLEGUIDE.md => conflicts/STYLEGUIDE.md (100%) rename build.gradle.kts => conflicts/build.gradle.kts (100%) rename docker-compose.yaml => conflicts/docker-compose.yaml (100%) rename {docs => conflicts/docs}/dev/changelog_updates.md (100%) rename {docs => conflicts/docs}/dev/checkstyle/checkstyle-config.xml (100%) rename gradle.properties => conflicts/gradle.properties (100%) rename {gradle => conflicts/gradle}/wrapper/gradle-wrapper.properties (100%) rename gradlew => conflicts/gradlew (100%) rename gradlew.bat => conflicts/gradlew.bat (100%) rename settings.gradle.kts => conflicts/settings.gradle.kts (100%) delete mode 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/.dockerignore b/conflicts/.dockerignore similarity index 100% rename from .dockerignore rename to conflicts/.dockerignore diff --git a/.editorconfig b/conflicts/.editorconfig similarity index 100% rename from .editorconfig rename to conflicts/.editorconfig diff --git a/.env b/conflicts/.env similarity index 100% rename from .env rename to conflicts/.env diff --git a/.gitattributes b/conflicts/.gitattributes similarity index 100% rename from .gitattributes rename to conflicts/.gitattributes diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/conflicts/.github/ISSUE_TEMPLATE/bug_report.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.yaml rename to conflicts/.github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/conflicts/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yml rename to conflicts/.github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/conflicts/.github/ISSUE_TEMPLATE/documentation.md similarity index 100% rename from .github/ISSUE_TEMPLATE/documentation.md rename to conflicts/.github/ISSUE_TEMPLATE/documentation.md diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/conflicts/.github/ISSUE_TEMPLATE/enhancement.md similarity index 100% rename from .github/ISSUE_TEMPLATE/enhancement.md rename to conflicts/.github/ISSUE_TEMPLATE/enhancement.md diff --git a/.github/ISSUE_TEMPLATE/epic_template.md b/conflicts/.github/ISSUE_TEMPLATE/epic_template.md similarity index 100% rename from .github/ISSUE_TEMPLATE/epic_template.md rename to conflicts/.github/ISSUE_TEMPLATE/epic_template.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/conflicts/.github/ISSUE_TEMPLATE/feature_request.md similarity index 100% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to conflicts/.github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/mds_feature_request.md b/conflicts/.github/ISSUE_TEMPLATE/mds_feature_request.md similarity index 100% rename from .github/ISSUE_TEMPLATE/mds_feature_request.md rename to conflicts/.github/ISSUE_TEMPLATE/mds_feature_request.md diff --git a/.github/ISSUE_TEMPLATE/process.md b/conflicts/.github/ISSUE_TEMPLATE/process.md similarity index 100% rename from .github/ISSUE_TEMPLATE/process.md rename to conflicts/.github/ISSUE_TEMPLATE/process.md diff --git a/.github/ISSUE_TEMPLATE/release.md b/conflicts/.github/ISSUE_TEMPLATE/release.md similarity index 100% rename from .github/ISSUE_TEMPLATE/release.md rename to conflicts/.github/ISSUE_TEMPLATE/release.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/conflicts/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to conflicts/.github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/add_issue_to_project.yml b/conflicts/.github/workflows/add_issue_to_project.yml similarity index 100% rename from .github/workflows/add_issue_to_project.yml rename to conflicts/.github/workflows/add_issue_to_project.yml diff --git a/.github/workflows/build-and-publish-connector-images.yml b/conflicts/.github/workflows/build-and-publish-connector-images.yml similarity index 100% rename from .github/workflows/build-and-publish-connector-images.yml rename to conflicts/.github/workflows/build-and-publish-connector-images.yml diff --git a/.github/workflows/build-and-publish-ts-api-client.yml b/conflicts/.github/workflows/build-and-publish-ts-api-client.yml similarity index 100% rename from .github/workflows/build-and-publish-ts-api-client.yml rename to conflicts/.github/workflows/build-and-publish-ts-api-client.yml diff --git a/.github/workflows/code_analysis.yml b/conflicts/.github/workflows/code_analysis.yml similarity index 100% rename from .github/workflows/code_analysis.yml rename to conflicts/.github/workflows/code_analysis.yml diff --git a/.github/workflows/codeql.yml b/conflicts/.github/workflows/codeql.yml similarity index 100% rename from .github/workflows/codeql.yml rename to conflicts/.github/workflows/codeql.yml diff --git a/.github/workflows/license_scan.yml b/conflicts/.github/workflows/license_scan.yml similarity index 100% rename from .github/workflows/license_scan.yml rename to conflicts/.github/workflows/license_scan.yml diff --git a/.github/workflows/release_docs_zip.yml b/conflicts/.github/workflows/release_docs_zip.yml similarity index 100% rename from .github/workflows/release_docs_zip.yml rename to conflicts/.github/workflows/release_docs_zip.yml diff --git a/.github/workflows/secret_scan.yml b/conflicts/.github/workflows/secret_scan.yml similarity index 100% rename from .github/workflows/secret_scan.yml rename to conflicts/.github/workflows/secret_scan.yml diff --git a/.github/workflows/security_scan.yml b/conflicts/.github/workflows/security_scan.yml similarity index 100% rename from .github/workflows/security_scan.yml rename to conflicts/.github/workflows/security_scan.yml diff --git a/.pre-commit-README.md b/conflicts/.pre-commit-README.md similarity index 100% rename from .pre-commit-README.md rename to conflicts/.pre-commit-README.md diff --git a/.pre-commit-config.yaml b/conflicts/.pre-commit-config.yaml similarity index 100% rename from .pre-commit-config.yaml rename to conflicts/.pre-commit-config.yaml diff --git a/CHANGELOG.md b/conflicts/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to conflicts/CHANGELOG.md diff --git a/CODE_OF_CONDUCT.md b/conflicts/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to conflicts/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/conflicts/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to conflicts/CONTRIBUTING.md diff --git a/LICENSE.md b/conflicts/LICENSE.md similarity index 100% rename from LICENSE.md rename to conflicts/LICENSE.md diff --git a/README.md b/conflicts/README.md similarity index 100% rename from README.md rename to conflicts/README.md diff --git a/SECURITY.md b/conflicts/SECURITY.md similarity index 100% rename from SECURITY.md rename to conflicts/SECURITY.md diff --git a/STYLEGUIDE.md b/conflicts/STYLEGUIDE.md similarity index 100% rename from STYLEGUIDE.md rename to conflicts/STYLEGUIDE.md diff --git a/build.gradle.kts b/conflicts/build.gradle.kts similarity index 100% rename from build.gradle.kts rename to conflicts/build.gradle.kts diff --git a/docker-compose.yaml b/conflicts/docker-compose.yaml similarity index 100% rename from docker-compose.yaml rename to conflicts/docker-compose.yaml diff --git a/docs/dev/changelog_updates.md b/conflicts/docs/dev/changelog_updates.md similarity index 100% rename from docs/dev/changelog_updates.md rename to conflicts/docs/dev/changelog_updates.md diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/conflicts/docs/dev/checkstyle/checkstyle-config.xml similarity index 100% rename from docs/dev/checkstyle/checkstyle-config.xml rename to conflicts/docs/dev/checkstyle/checkstyle-config.xml diff --git a/gradle.properties b/conflicts/gradle.properties similarity index 100% rename from gradle.properties rename to conflicts/gradle.properties diff --git a/gradle/wrapper/gradle-wrapper.properties b/conflicts/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to conflicts/gradle/wrapper/gradle-wrapper.properties diff --git a/gradlew b/conflicts/gradlew similarity index 100% rename from gradlew rename to conflicts/gradlew diff --git a/gradlew.bat b/conflicts/gradlew.bat similarity index 100% rename from gradlew.bat rename to conflicts/gradlew.bat diff --git a/settings.gradle.kts b/conflicts/settings.gradle.kts similarity index 100% rename from settings.gradle.kts rename to conflicts/settings.gradle.kts diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a4d4fb3f96a785543079b8df6723c946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59821 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 From b237de35c0e152066439a6d8094c139bff9bb028 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 12 Dec 2022 07:55:24 +0100 Subject: [PATCH 217/295] squashed edc-ce history before broker/crawler merge Co-authored-by: AbdullahMuk <143605455+AbdullahMuk@users.noreply.github.com> Co-authored-by: AnurosePrakash <108627760+AnurosePrakash@users.noreply.github.com> Co-authored-by: Christophe Loiseau <116@lab0.net> Co-authored-by: Christophe Loiseau Co-authored-by: Christophe Loiseau Co-authored-by: Daniel Vangrieken Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: efiege <105237007+efiege@users.noreply.github.com> Co-authored-by: Jan Ridderbusch <36418748+jridderbusch@users.noreply.github.com> Co-authored-by: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Co-authored-by: kulgg <75735874+kulgg@users.noreply.github.com> Co-authored-by: Omar Silva <77329033+omarsilva1@users.noreply.github.com> Co-authored-by: Richard Treier Co-authored-by: Ronja Quensel <72978761+ronjaquensel@users.noreply.github.com> Co-authored-by: Saad Bendou Co-authored-by: Sebastian Opriel <22075788+SebastianOpriel@users.noreply.github.com> Co-authored-by: Sebastian Opriel Co-authored-by: sovitybot <107936402+sovitybot@users.noreply.github.com> Co-authored-by: Thilo Schaper Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Co-authored-by: Tim Berthold --- .dockerignore | 50 + .editorconfig | 10 + .env | 5 + .env.dev | 6 + .gitattributes | 102 + .github/ISSUE_TEMPLATE/bug_report.yaml | 62 + .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/documentation.md | 30 + .github/ISSUE_TEMPLATE/enhancement.md | 27 + .github/ISSUE_TEMPLATE/epic_template.md | 41 + .github/ISSUE_TEMPLATE/feature_request.md | 34 + .github/ISSUE_TEMPLATE/feature_request_mds.md | 39 + .github/ISSUE_TEMPLATE/process.md | 24 + .github/ISSUE_TEMPLATE/release.md | 72 + .github/PULL_REQUEST_TEMPLATE.md | 10 + .../actions/build-connector-image/action.yml | 75 + .github/markdown-link-checker-config.jq | 21 + .github/workflows/add_issue_to_project.yml | 16 + .github/workflows/ci.yml | 168 + .github/workflows/code_analysis.yml | 59 + .github/workflows/codeql.yml | 49 + .github/workflows/license_scan.yml | 41 + .github/workflows/secret_scan.yml | 25 + .github/workflows/security_scan.yml | 43 + .github/workflows/trivy.yml | 32 + .gitignore | 49 + .pre-commit-README.md | 25 + .pre-commit-config.yaml | 7 + CHANGELOG.md | 743 ++++ CODE_OF_CONDUCT.md | 70 + CONTRIBUTING.md | 162 + LICENSE | 201 + README.md | 168 + SECURITY.md | 32 + STYLEGUIDE.md | 58 + SUMMARY.md | 48 + UPDATES.md | 32 + build.gradle.kts | 125 + docker-compose-dev.yaml | 130 + docker-compose.yaml | 130 + .../goals/development/README.md | 69 + .../goals/local-demo/4.2.0/README.md | 63 + .../goals/local-demo/README.md | 62 + .../goals/production/4.2.0/README.md | 183 + .../production/4.2.0/public-endpoints.yaml | 30 + .../goals/production/README.md | 244 ++ .../goals/production/generate_ski_aki.sh | 42 + .../goals/production/public-endpoints.yaml | 650 +++ docs/dev/changelog_updates.md | 60 + docs/dev/checkstyle/checkstyle-config.xml | 416 ++ docs/eclipse-edc-management-api.yaml | 2818 +++++++++++++ docs/getting-started/README.md | 12 + .../documentation/api_wrapper.md | 45 + .../documentation/data-transfer-methods.md | 15 + .../images/data-transfer-methods.png | Bin 0 -> 2507761 bytes .../documentation/oauth-data-address.md | 129 + .../documentation/parameterized_assets.md | 51 + .../parameterized_assets_via_ui.md | 56 + .../documentation/pull-data-transfer.md | 97 + .../screenshots/parameterized-asset.png | Bin 0 -> 26510 bytes docs/postman_collection.json | 904 ++++ docs/sovity-edc-api-wrapper.yaml | 1577 +++++++ extensions/edc-ui-config/README.md | 36 + extensions/edc-ui-config/build.gradle.kts | 57 + .../edc/extension/EdcUiConfigController.java | 37 + .../edc/extension/EdcUiConfigExtension.java | 44 + .../edc/extension/EdcUiConfigService.java | 42 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../version/controller/EdcUiConfigTest.java | 55 + .../version/controller/TestUtils.java | 56 + extensions/last-commit-info/README.md | 64 + extensions/last-commit-info/build.gradle.kts | 64 + .../sovity/edc/extension/LastCommitInfo.java | 27 + .../extension/LastCommitInfoController.java | 36 + .../extension/LastCommitInfoExtension.java | 43 + .../edc/extension/LastCommitInfoService.java | 69 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../src/main/resources/jar-build-date.txt | 1 + .../main/resources/jar-last-commit-info.txt | 3 + .../controller/LastCommitInfoTest.java | 74 + .../version/controller/TestUtils.java | 26 + .../src/test/resources/jar-build-date.txt | 1 + .../test/resources/jar-last-commit-info.txt | 1 + extensions/policy-always-true/README.md | 31 + .../policy-always-true/build.gradle.kts | 33 + .../policy/AlwaysTruePolicyConstants.java | 25 + .../policy/AlwaysTruePolicyExtension.java | 66 + .../AlwaysTruePolicyDefinitionService.java | 71 + .../services/AlwaysTruePolicyService.java | 52 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../policy/AlwaysTruePolicyExtensionTest.java | 78 + .../policy-referring-connector/README.md | 36 + .../build.gradle.kts | 35 + ...ReferringConnectorValidationExtension.java | 116 + .../AbstractReferringConnectorValidation.java | 140 + .../ReferringConnectorDutyFunction.java | 45 + .../ReferringConnectorPermissionFunction.java | 45 + ...ReferringConnectorProhibitionFunction.java | 45 + ...rg.eclipse.edc.spi.system.ServiceExtension | 23 + ...rringConnectorValidationExtensionTest.java | 105 + ...tractReferringConnectorValidationTest.java | 195 + extensions/policy-time-interval/README.md | 35 + .../policy-time-interval/build.gradle.kts | 24 + .../policy/PolicyEvaluationTimeExtension.java | 54 + .../policy/PolicyEvaluationTimeFunction.java | 52 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + extensions/postgres-flyway/README.md | 62 + extensions/postgres-flyway/build.gradle.kts | 42 + .../postgresql/PostgresFlywayExtension.java | 50 + .../DriverManagerConnectionFactory.java | 47 + .../connection/JdbcConnectionProperties.java | 50 + .../migration/DatabaseMigrationManager.java | 72 + .../postgresql/migration/FlywayService.java | 144 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../migration/asset/V0_0_1__Asset_Schema.sql | 52 + .../migration/asset/V0_0_2__Asset_Schema.sql | 21 + .../V0_0_1__ContractDefinition_Schema.sql | 24 + .../V0_0_2__ContractDefinition_Schema.sql | 20 + .../V0_0_3__ContractDefinition_Schema.sql | 14 + .../V0_0_4__Set_Default_Validity.sql | 14 + .../V0_0_1__ContractNegotiation_Schema.sql | 78 + .../V0_0_2__ContractNegotiation_Schema.sql | 24 + .../V0_0_3__Fix_Contract_Offer_JSON.sql | 37 + .../V0_0_1__DataplaneInstance_Schema.sql | 19 + .../default/V1_0_0_Example_Migration.sql | 5 + .../V2__Delete-Transfer-Processes-Trigger.sql | 41 + .../migration/default/V3__MS8-to-0.2.1.sql | 416 ++ .../default/V4__MS8-to-0.2.1_bugfixes.sql | 48 + .../default/V5__Mobility_DCAT_Mapping.sql | 76 + .../default/V6__Fix_DataModel_ID_Field.sql | 63 + .../policy/V0_0_1__Policy_Schema.sql | 39 + .../policy/V0_0_2__Policy_Schema.sql | 20 + .../V0_0_1__TransferProcess_Schema.sql | 86 + .../V0_0_2__TransferProcess_Schema.sql | 28 + .../V0_0_3__TransferProcess_Schema.sql | 14 + .../V0_0_4__Set_Default_Properties.sql | 14 + .../sovity-edc-extensions-package/README.md | 33 + .../build.gradle.kts | 31 + extensions/test-backend-controller/README.md | 29 + .../test-backend-controller/build.gradle.kts | 23 + .../TestBackendController.java | 72 + .../TestBackendExtension.java | 34 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../transfer-process-status-checker/README.md | 31 + .../build.gradle.kts | 23 + ...TransferProcessStatusCheckerExtension.java | 43 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + extensions/wrapper/README.md | 53 + .../clients/java-client-example/.gitignore | 36 + .../clients/java-client-example/README.md | 30 + .../java-client-example/build.gradle.kts | 40 + .../edc/client/examples/EdcClientSetup.java | 53 + .../edc/client/examples/GreetingResource.java | 44 + .../src/main/resources/application.properties | 15 + .../client/examples/GreetingResourceTest.java | 59 + .../wrapper/clients/java-client/README.md | 112 + .../clients/java-client/build.gradle.kts | 129 + .../java/de/sovity/edc/client/EdcClient.java | 40 + .../sovity/edc/client/EdcClientBuilder.java | 55 + .../sovity/edc/client/EdcClientFactory.java | 65 + .../oauth2/OAuth2ClientCredentials.java | 36 + .../OAuth2CredentialsAuthenticator.java | 59 + .../oauth2/OAuth2CredentialsInterceptor.java | 40 + .../client/oauth2/OAuth2CredentialsStore.java | 54 + .../edc/client/oauth2/OAuth2TokenFetcher.java | 56 + .../client/oauth2/OAuth2TokenResponse.java | 35 + .../edc/client/oauth2/OkHttpRequestUtils.java | 38 + .../edc/client/oauth2/SovityKeycloakUrl.java | 37 + .../typescript-client-example/.gitignore | 10 + .../typescript-client-example/.prettierignore | 13 + .../typescript-client-example/.prettierrc | 17 + .../typescript-client-example/README.md | 27 + .../package-lock.json | 3680 +++++++++++++++++ .../typescript-client-example/package.json | 33 + .../postcss.config.js | 6 + .../typescript-client-example/src/app.css | 9 + .../typescript-client-example/src/app.d.ts | 12 + .../typescript-client-example/src/app.html | 12 + .../src/routes/+layout.svelte | 5 + .../src/routes/+page.svelte | 75 + .../static/favicon.png | Bin 0 -> 1571 bytes .../svelte.config.js | 18 + .../tailwind.config.js | 8 + .../typescript-client-example/tsconfig.json | 17 + .../typescript-client-example/vite.config.ts | 6 + .../clients/typescript-client/.gitignore | 24 + .../clients/typescript-client/.prettierignore | 2 + .../clients/typescript-client/README.md | 73 + .../clients/typescript-client/index.html | 12 + .../typescript-client/package-lock.json | 3222 +++++++++++++++ .../clients/typescript-client/package.json | 56 + .../typescript-client/prettier.config.cjs | 26 + .../typescript-client/src/EdcClient.ts | 65 + .../src/generated/.gitignore | 2 + .../clients/typescript-client/src/index.ts | 2 + .../src/oauth2/AccessTokenService.ts | 47 + .../src/oauth2/Middleware.ts | 34 + .../src/oauth2/model/ClientCredentials.ts | 8 + .../src/oauth2/utils/FetchUtils.ts | 39 + .../src/oauth2/utils/HttpUtils.ts | 13 + .../src/oauth2/utils/RequestUtils.ts | 44 + .../typescript-client/src/vite-env.d.ts | 1 + .../clients/typescript-client/tsconfig.json | 19 + .../clients/typescript-client/vite.config.ts | 17 + extensions/wrapper/wrapper-api/README.md | 27 + .../wrapper/wrapper-api/build.gradle.kts | 98 + .../edc/ext/wrapper/api/ApiInformation.java | 50 + .../edc/ext/wrapper/api/ui/UiResource.java | 173 + .../ext/wrapper/api/ui/model/AssetPage.java | 29 + .../api/ui/model/ContractAgreementCard.java | 62 + .../ui/model/ContractAgreementDirection.java | 40 + .../api/ui/model/ContractAgreementPage.java | 28 + .../ContractAgreementTransferProcess.java | 41 + .../api/ui/model/ContractDefinitionEntry.java | 37 + .../api/ui/model/ContractDefinitionPage.java | 30 + .../ui/model/ContractDefinitionRequest.java | 46 + .../ui/model/ContractNegotiationRequest.java | 47 + .../ContractNegotiationSimplifiedState.java | 29 + .../ui/model/ContractNegotiationState.java | 37 + .../api/ui/model/DashboardDapsConfig.java | 17 + .../api/ui/model/DashboardMiwConfig.java | 20 + .../wrapper/api/ui/model/DashboardPage.java | 68 + .../ui/model/DashboardTransferAmounts.java | 28 + .../wrapper/api/ui/model/IdResponseDto.java | 37 + .../model/InitiateCustomTransferRequest.java | 38 + .../api/ui/model/InitiateTransferRequest.java | 41 + .../api/ui/model/PolicyDefinitionPage.java | 30 + .../api/ui/model/TransferHistoryEntry.java | 58 + .../api/ui/model/TransferHistoryPage.java | 28 + .../model/TransferProcessSimplifiedState.java | 29 + .../api/ui/model/TransferProcessState.java | 37 + .../api/ui/model/UiContractNegotiation.java | 46 + .../wrapper/api/ui/model/UiContractOffer.java | 30 + .../ext/wrapper/api/ui/model/UiCriterion.java | 36 + .../api/ui/model/UiCriterionLiteral.java | 36 + .../api/ui/model/UiCriterionLiteralType.java | 8 + .../api/ui/model/UiCriterionOperator.java | 32 + .../ext/wrapper/api/ui/model/UiDataOffer.java | 37 + .../wrapper/api/usecase/UseCaseResource.java | 46 + .../wrapper/api/usecase/model/KpiResult.java | 46 + .../model/TransferProcessStatesDto.java | 32 + .../wrapper/wrapper-common-api/README.md | 38 + .../wrapper-common-api/build.gradle.kts | 30 + .../wrapper/api/common/model/AssetDto.java | 44 + .../api/common/model/AtomicConstraintDto.java | 49 + .../api/common/model/CriterionDto.java | 33 + .../api/common/model/ExpressionDto.java | 49 + .../api/common/model/ExpressionType.java | 11 + .../wrapper/api/common/model/OperatorDto.java | 37 + .../api/common/model/PermissionDto.java | 39 + .../model/PolicyDefinitionCreateRequest.java | 39 + .../api/common/model/PolicyDefinitionDto.java | 39 + .../ext/wrapper/api/common/model/UiAsset.java | 162 + .../common/model/UiAssetCreateRequest.java | 132 + .../model/UiAssetEditMetadataRequest.java | 125 + .../wrapper/api/common/model/UiPolicy.java | 47 + .../api/common/model/UiPolicyConstraint.java | 42 + .../common/model/UiPolicyCreateRequest.java | 38 + .../api/common/model/UiPolicyLiteral.java | 70 + .../api/common/model/UiPolicyLiteralType.java | 24 + .../wrapper/wrapper-common-mappers/README.md | 34 + .../wrapper-common-mappers/build.gradle.kts | 50 + .../api/common/mappers/AssetMapper.java | 67 + .../api/common/mappers/OperatorMapper.java | 34 + .../api/common/mappers/PolicyMapper.java | 112 + .../mappers/utils/AssetJsonLdUtils.java | 35 + .../mappers/utils/AtomicConstraintMapper.java | 106 + .../mappers/utils/ConstraintExtractor.java | 113 + .../mappers/utils/EdcPropertyUtils.java | 75 + .../mappers/utils/FailedMappingException.java | 26 + .../mappers/utils/JsonBuilderUtils.java | 79 + .../common/mappers/utils/LiteralMapper.java | 109 + .../common/mappers/utils/MappingErrors.java | 52 + .../utils/MarkdownToTextConverter.java | 30 + .../utils/OwnConnectorEndpointService.java | 20 + .../ParameterizationCompatibilityUtils.java | 58 + .../common/mappers/utils/PolicyValidator.java | 115 + .../api/common/mappers/utils/TextUtils.java | 23 + .../common/mappers/utils/UiAssetMapper.java | 374 ++ .../api/common/mappers/AssetMapperTest.java | 263 ++ .../common/mappers/OperatorMapperTest.java | 52 + .../api/common/mappers/PolicyMapperTest.java | 91 + .../wrapper/api/common/mappers/TestUtils.java | 15 + .../utils/AtomicConstraintMapperTest.java | 240 ++ .../utils/ConstraintExtractorTest.java | 89 + .../mappers/utils/EdcPropertyUtilsTest.java | 64 + .../mappers/utils/LiteralMapperTest.java | 254 ++ .../mappers/utils/MappingErrorsTest.java | 27 + .../mappers/utils/PolicyValidatorTest.java | 158 + .../common/mappers/utils/TextUtilsTest.java | 63 + .../mappers/utils/UiAssetMapperTest.java | 306 ++ .../src/test/resources/example-asset.jsonld | 82 + extensions/wrapper/wrapper-ee-api/README.md | 30 + .../wrapper/wrapper-ee-api/build.gradle.kts | 33 + .../api/ee/EnterpriseEditionResource.java | 72 + .../wrapper/api/ee/model/ConnectorLimits.java | 37 + .../ext/wrapper/api/ee/model/StoredFile.java | 75 + extensions/wrapper/wrapper/README.md | 40 + extensions/wrapper/wrapper/build.gradle.kts | 87 + .../edc/ext/wrapper/WrapperExtension.java | 126 + .../ext/wrapper/WrapperExtensionContext.java | 34 + .../WrapperExtensionContextBuilder.java | 277 ++ .../edc/ext/wrapper/api/ApiInformation.java | 46 + .../edc/ext/wrapper/api/ServiceException.java | 32 + .../ext/wrapper/api/ui/UiResourceImpl.java | 157 + .../api/ui/pages/asset/AssetApiService.java | 75 + .../api/ui/pages/asset/AssetBuilder.java | 104 + .../api/ui/pages/asset/AssetIdValidator.java | 31 + .../ui/pages/catalog/CatalogApiService.java | 75 + .../ContractAgreementPageApiService.java | 43 + .../ContractAgreementTransferApiService.java | 52 + .../services/ContractAgreementData.java | 40 + .../ContractAgreementDataFetcher.java | 104 + .../ContractAgreementPageCardBuilder.java | 90 + .../services/ContractAgreementUtils.java | 34 + .../services/ContractNegotiationUtils.java | 71 + .../services/TransferRequestBuilder.java | 102 + .../ContractDefinitionApiService.java | 71 + .../ContractDefinitionBuilder.java | 40 + .../CriterionLiteralMapper.java | 51 + .../contract_definitions/CriterionMapper.java | 54 + .../CriterionOperatorMapper.java | 50 + .../ContractNegotiationApiService.java | 63 + .../ContractNegotiationBuilder.java | 39 + .../ContractNegotiationStateService.java | 85 + .../ContractOfferMapper.java | 35 + .../dashboard/DashboardPageApiService.java | 116 + .../services/ConfigPropertyUtils.java | 18 + .../dashboard/services/DapsConfigService.java | 42 + .../services/DashboardDataFetcher.java | 63 + .../dashboard/services/MiwConfigService.java | 45 + .../OwnConnectorEndpointServiceImpl.java | 28 + .../services/SelfDescriptionService.java | 118 + .../policy/PolicyDefinitionApiService.java | 78 + .../TransferHistoryPageApiService.java | 144 + ...ransferHistoryPageAssetFetcherService.java | 70 + .../TransferProcessStateService.java | 85 + .../api/usecase/UseCaseResourceImpl.java | 42 + .../api/usecase/services/KpiApiService.java | 101 + .../services/SupportedPolicyApiService.java | 37 + .../edc/ext/wrapper/utils/EdcDateUtils.java | 57 + .../ext/wrapper/utils/FieldAccessUtils.java | 62 + .../edc/ext/wrapper/utils/MapUtils.java | 43 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../de/sovity/edc/ext/wrapper/TestUtils.java | 85 + .../ui/pages/asset/AssetApiServiceTest.java | 449 ++ .../ui/pages/asset/AssetIdValidatorTest.java | 53 + .../api/ui/pages/catalog/CatalogApiTest.java | 110 + .../ContractAgreementPageTest.java | 209 + ...ntractAgreementTransferApiServiceTest.java | 178 + .../services/TransferRequestBuilderTest.java | 106 + .../ContractDefinitionPageApiServiceTest.java | 184 + .../CriterionLiteralMapperTest.java | 55 + .../CriterionMapperTest.java | 44 + .../CriterionOperatorMapperTest.java | 38 + .../ContractNegotiationStateServiceTest.java | 87 + .../DashboardPageApiServiceTest.java | 221 + .../PolicyDefinitionApiServiceTest.java | 131 + .../TransferHistoryPageApiServiceTest.java | 116 + .../TransferProcessAssetApiServiceTest.java | 79 + .../TransferProcessStateServiceTest.java | 87 + .../TransferProcessTestUtils.java | 165 + .../ext/wrapper/api/usecase/KpiApiTest.java | 51 + .../api/usecase/SupportedPolicyApiTest.java | 46 + .../edc/ext/wrapper/utils/MapUtilsTest.java | 40 + gradle.properties | 26 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 ++ gradlew.bat | 89 + launchers/.env | 97 + launchers/Dockerfile | 37 + launchers/README.md | 62 + launchers/common/auth-daps/build.gradle.kts | 15 + launchers/common/auth-mock/build.gradle.kts | 14 + launchers/common/base-mds/build.gradle.kts | 10 + launchers/common/base/build.gradle.kts | 40 + .../common/observability/build.gradle.kts | 14 + launchers/connectors/mds-ce/build.gradle.kts | 27 + .../connectors/sovity-ce/build.gradle.kts | 26 + .../connectors/sovity-dev/build.gradle.kts | 23 + .../connectors/test-backend/build.gradle.kts | 28 + launchers/docker-entrypoint.sh | 43 + launchers/logging.dev.properties | 7 + launchers/logging.properties | 7 + settings.gradle.kts | 32 + tests/build.gradle.kts | 41 + .../de/sovity/edc/e2e/ApiWrapperDemoTest.java | 223 + .../e2e/DataSourceParameterizationTest.java | 560 +++ .../edc/e2e/DataSourceQueryParamsTest.java | 214 + .../edc/e2e/ManagementApiTransferTest.java | 81 + .../edc/e2e/Ms8ConnectorMigrationTest.java | 231 ++ .../de/sovity/edc/e2e/UiApiWrapperTest.java | 606 +++ .../V1_9__ms8-test-contract-consumer.sql | 67 + .../V1_9__ms8-test-contract-provider.sql | 292 ++ utils/catalog-parser/README.md | 31 + utils/catalog-parser/build.gradle.kts | 49 + .../edc/utils/catalog/DspCatalogService.java | 50 + .../catalog/DspCatalogServiceException.java | 26 + .../catalog/mapper/DspContractOfferUtils.java | 78 + .../catalog/mapper/DspDataOfferBuilder.java | 71 + .../edc/utils/catalog/model/DspCatalog.java | 25 + .../utils/catalog/model/DspContractOffer.java | 23 + .../edc/utils/catalog/model/DspDataOffer.java | 26 + .../utils/catalog/DspCatalogServiceTest.java | 89 + .../edc/utils/catalog/catalogResponse.json | 47 + utils/json-and-jsonld-utils/README.md | 31 + utils/json-and-jsonld-utils/build.gradle.kts | 44 + .../java/de/sovity/edc/utils/JsonUtils.java | 52 + .../sovity/edc/utils/jsonld/JsonLdUtils.java | 313 ++ .../sovity/edc/utils/jsonld/vocab/Prop.java | 196 + utils/test-connector-remote/README.md | 31 + utils/test-connector-remote/build.gradle.kts | 42 + .../e2e/connector/ConnectorRemote.java | 386 ++ .../e2e/connector/DataTransferTestUtil.java | 80 + .../e2e/connector/MockDataAddressRemote.java | 56 + .../e2e/connector/config/ConnectorConfig.java | 39 + .../config/ConnectorConfigFactory.java | 65 + .../config/ConnectorRemoteConfig.java | 28 + .../config/ConnectorRemoteConfigFactory.java | 83 + .../config/DatasourceConfigUtils.java | 49 + .../connector/config/api/EdcApiConfig.java | 38 + .../config/api/EdcApiConfigFactory.java | 89 + .../e2e/connector/config/api/EdcApiGroup.java | 44 + .../config/api/EdcApiGroupConfig.java | 38 + .../config/api/auth/ApiKeyAuthProvider.java | 29 + .../config/api/auth/AuthProvider.java | 21 + .../config/api/auth/NoneAuthProvider.java | 26 + .../edc/extension/e2e/db/JdbcCredentials.java | 29 + .../edc/extension/e2e/db/TestDatabase.java | 34 + .../extension/e2e/db/TestDatabaseFactory.java | 35 + .../e2e/db/TestDatabaseViaEnvVars.java | 41 + .../e2e/db/TestDatabaseViaTestcontainers.java | 48 + .../sovity/edc/extension/e2e/env/EnvUtil.java | 41 + utils/test-utils/build.gradle.kts | 26 + .../utils/junit/DisabledOnGithub.java | 31 + 436 files changed, 40861 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .env.dev create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/enhancement.md create mode 100644 .github/ISSUE_TEMPLATE/epic_template.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request_mds.md create mode 100644 .github/ISSUE_TEMPLATE/process.md create mode 100644 .github/ISSUE_TEMPLATE/release.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/actions/build-connector-image/action.yml create mode 100755 .github/markdown-link-checker-config.jq create mode 100644 .github/workflows/add_issue_to_project.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/code_analysis.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/license_scan.yml create mode 100644 .github/workflows/secret_scan.yml create mode 100644 .github/workflows/security_scan.yml create mode 100644 .github/workflows/trivy.yml create mode 100644 .gitignore create mode 100644 .pre-commit-README.md create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 STYLEGUIDE.md create mode 100644 SUMMARY.md create mode 100644 UPDATES.md create mode 100644 build.gradle.kts create mode 100644 docker-compose-dev.yaml create mode 100644 docker-compose.yaml create mode 100644 docs/deployment-guide/goals/development/README.md create mode 100644 docs/deployment-guide/goals/local-demo/4.2.0/README.md create mode 100644 docs/deployment-guide/goals/local-demo/README.md create mode 100644 docs/deployment-guide/goals/production/4.2.0/README.md create mode 100644 docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml create mode 100644 docs/deployment-guide/goals/production/README.md create mode 100644 docs/deployment-guide/goals/production/generate_ski_aki.sh create mode 100644 docs/deployment-guide/goals/production/public-endpoints.yaml create mode 100644 docs/dev/changelog_updates.md create mode 100644 docs/dev/checkstyle/checkstyle-config.xml create mode 100644 docs/eclipse-edc-management-api.yaml create mode 100644 docs/getting-started/README.md create mode 100644 docs/getting-started/documentation/api_wrapper.md create mode 100644 docs/getting-started/documentation/data-transfer-methods.md create mode 100644 docs/getting-started/documentation/images/data-transfer-methods.png create mode 100644 docs/getting-started/documentation/oauth-data-address.md create mode 100644 docs/getting-started/documentation/parameterized_assets.md create mode 100644 docs/getting-started/documentation/parameterized_assets_via_ui.md create mode 100644 docs/getting-started/documentation/pull-data-transfer.md create mode 100644 docs/getting-started/documentation/screenshots/parameterized-asset.png create mode 100644 docs/postman_collection.json create mode 100644 docs/sovity-edc-api-wrapper.yaml create mode 100644 extensions/edc-ui-config/README.md create mode 100644 extensions/edc-ui-config/build.gradle.kts create mode 100644 extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigController.java create mode 100644 extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigExtension.java create mode 100644 extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigService.java create mode 100644 extensions/edc-ui-config/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/EdcUiConfigTest.java create mode 100644 extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java create mode 100644 extensions/last-commit-info/README.md create mode 100644 extensions/last-commit-info/build.gradle.kts create mode 100644 extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfo.java create mode 100644 extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoController.java create mode 100644 extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoExtension.java create mode 100644 extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoService.java create mode 100644 extensions/last-commit-info/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/last-commit-info/src/main/resources/jar-build-date.txt create mode 100644 extensions/last-commit-info/src/main/resources/jar-last-commit-info.txt create mode 100644 extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/LastCommitInfoTest.java create mode 100644 extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java create mode 100644 extensions/last-commit-info/src/test/resources/jar-build-date.txt create mode 100644 extensions/last-commit-info/src/test/resources/jar-last-commit-info.txt create mode 100644 extensions/policy-always-true/README.md create mode 100644 extensions/policy-always-true/build.gradle.kts create mode 100644 extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyConstants.java create mode 100644 extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtension.java create mode 100644 extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java create mode 100644 extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyService.java create mode 100644 extensions/policy-always-true/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java create mode 100644 extensions/policy-referring-connector/README.md create mode 100644 extensions/policy-referring-connector/build.gradle.kts create mode 100644 extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtension.java create mode 100644 extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java create mode 100644 extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorDutyFunction.java create mode 100644 extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorPermissionFunction.java create mode 100644 extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorProhibitionFunction.java create mode 100644 extensions/policy-referring-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtensionTest.java create mode 100644 extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java create mode 100644 extensions/policy-time-interval/README.md create mode 100644 extensions/policy-time-interval/build.gradle.kts create mode 100644 extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeExtension.java create mode 100644 extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeFunction.java create mode 100644 extensions/policy-time-interval/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/postgres-flyway/README.md create mode 100644 extensions/postgres-flyway/build.gradle.kts create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java create mode 100644 extensions/postgres-flyway/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_1__Asset_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_2__Asset_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/default/V1_0_0_Example_Migration.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/default/V2__Delete-Transfer-Processes-Trigger.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/default/V3__MS8-to-0.2.1.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/default/V4__MS8-to-0.2.1_bugfixes.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/default/V5__Mobility_DCAT_Mapping.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/default/V6__Fix_DataModel_ID_Field.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_1__Policy_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_2__Policy_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql create mode 100644 extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_4__Set_Default_Properties.sql create mode 100644 extensions/sovity-edc-extensions-package/README.md create mode 100644 extensions/sovity-edc-extensions-package/build.gradle.kts create mode 100644 extensions/test-backend-controller/README.md create mode 100644 extensions/test-backend-controller/build.gradle.kts create mode 100644 extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java create mode 100644 extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java create mode 100644 extensions/test-backend-controller/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/transfer-process-status-checker/README.md create mode 100644 extensions/transfer-process-status-checker/build.gradle.kts create mode 100644 extensions/transfer-process-status-checker/src/main/java/de/sovity/edc/extension/transfer/TransferProcessStatusCheckerExtension.java create mode 100644 extensions/transfer-process-status-checker/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/wrapper/README.md create mode 100644 extensions/wrapper/clients/java-client-example/.gitignore create mode 100644 extensions/wrapper/clients/java-client-example/README.md create mode 100644 extensions/wrapper/clients/java-client-example/build.gradle.kts create mode 100644 extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/EdcClientSetup.java create mode 100644 extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/GreetingResource.java create mode 100644 extensions/wrapper/clients/java-client-example/src/main/resources/application.properties create mode 100644 extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java create mode 100644 extensions/wrapper/clients/java-client/README.md create mode 100644 extensions/wrapper/clients/java-client/build.gradle.kts create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClient.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientFactory.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java create mode 100644 extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java create mode 100644 extensions/wrapper/clients/typescript-client-example/.gitignore create mode 100644 extensions/wrapper/clients/typescript-client-example/.prettierignore create mode 100644 extensions/wrapper/clients/typescript-client-example/.prettierrc create mode 100644 extensions/wrapper/clients/typescript-client-example/README.md create mode 100644 extensions/wrapper/clients/typescript-client-example/package-lock.json create mode 100644 extensions/wrapper/clients/typescript-client-example/package.json create mode 100644 extensions/wrapper/clients/typescript-client-example/postcss.config.js create mode 100644 extensions/wrapper/clients/typescript-client-example/src/app.css create mode 100644 extensions/wrapper/clients/typescript-client-example/src/app.d.ts create mode 100644 extensions/wrapper/clients/typescript-client-example/src/app.html create mode 100644 extensions/wrapper/clients/typescript-client-example/src/routes/+layout.svelte create mode 100644 extensions/wrapper/clients/typescript-client-example/src/routes/+page.svelte create mode 100644 extensions/wrapper/clients/typescript-client-example/static/favicon.png create mode 100644 extensions/wrapper/clients/typescript-client-example/svelte.config.js create mode 100644 extensions/wrapper/clients/typescript-client-example/tailwind.config.js create mode 100644 extensions/wrapper/clients/typescript-client-example/tsconfig.json create mode 100644 extensions/wrapper/clients/typescript-client-example/vite.config.ts create mode 100644 extensions/wrapper/clients/typescript-client/.gitignore create mode 100644 extensions/wrapper/clients/typescript-client/.prettierignore create mode 100644 extensions/wrapper/clients/typescript-client/README.md create mode 100644 extensions/wrapper/clients/typescript-client/index.html create mode 100644 extensions/wrapper/clients/typescript-client/package-lock.json create mode 100644 extensions/wrapper/clients/typescript-client/package.json create mode 100644 extensions/wrapper/clients/typescript-client/prettier.config.cjs create mode 100644 extensions/wrapper/clients/typescript-client/src/EdcClient.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/generated/.gitignore create mode 100644 extensions/wrapper/clients/typescript-client/src/index.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/oauth2/AccessTokenService.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/oauth2/Middleware.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/oauth2/model/ClientCredentials.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/oauth2/utils/FetchUtils.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/oauth2/utils/HttpUtils.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/oauth2/utils/RequestUtils.ts create mode 100644 extensions/wrapper/clients/typescript-client/src/vite-env.d.ts create mode 100644 extensions/wrapper/clients/typescript-client/tsconfig.json create mode 100644 extensions/wrapper/clients/typescript-client/vite.config.ts create mode 100644 extensions/wrapper/wrapper-api/README.md create mode 100644 extensions/wrapper/wrapper-api/build.gradle.kts create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementDirection.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationSimplifiedState.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardMiwConfig.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessSimplifiedState.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteralType.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java create mode 100644 extensions/wrapper/wrapper-common-api/README.md create mode 100644 extensions/wrapper/wrapper-common-api/build.gradle.kts create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteralType.java create mode 100644 extensions/wrapper/wrapper-common-mappers/README.md create mode 100644 extensions/wrapper/wrapper-common-mappers/build.gradle.kts create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld create mode 100644 extensions/wrapper/wrapper-ee-api/README.md create mode 100644 extensions/wrapper/wrapper-ee-api/build.gradle.kts create mode 100644 extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/EnterpriseEditionResource.java create mode 100644 extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/ConnectorLimits.java create mode 100644 extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/StoredFile.java create mode 100644 extensions/wrapper/wrapper/README.md create mode 100644 extensions/wrapper/wrapper/build.gradle.kts create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ServiceException.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidator.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementUtils.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractNegotiationUtils.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractOfferMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/ConfigPropertyUtils.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DapsConfigService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DashboardDataFetcher.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/MiwConfigService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/SelfDescriptionService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/KpiApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/SupportedPolicyApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/EdcDateUtils.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/FieldAccessUtils.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/MapUtils.java create mode 100644 extensions/wrapper/wrapper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidatorTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapperTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapperTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapperTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateServiceTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/utils/MapUtilsTest.java create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 launchers/.env create mode 100644 launchers/Dockerfile create mode 100644 launchers/README.md create mode 100644 launchers/common/auth-daps/build.gradle.kts create mode 100644 launchers/common/auth-mock/build.gradle.kts create mode 100644 launchers/common/base-mds/build.gradle.kts create mode 100644 launchers/common/base/build.gradle.kts create mode 100644 launchers/common/observability/build.gradle.kts create mode 100644 launchers/connectors/mds-ce/build.gradle.kts create mode 100644 launchers/connectors/sovity-ce/build.gradle.kts create mode 100644 launchers/connectors/sovity-dev/build.gradle.kts create mode 100644 launchers/connectors/test-backend/build.gradle.kts create mode 100755 launchers/docker-entrypoint.sh create mode 100644 launchers/logging.dev.properties create mode 100644 launchers/logging.properties create mode 100644 settings.gradle.kts create mode 100644 tests/build.gradle.kts create mode 100644 tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java create mode 100644 tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql create mode 100644 tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql create mode 100644 utils/catalog-parser/README.md create mode 100644 utils/catalog-parser/build.gradle.kts create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java create mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java create mode 100644 utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java create mode 100644 utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json create mode 100644 utils/json-and-jsonld-utils/README.md create mode 100644 utils/json-and-jsonld-utils/build.gradle.kts create mode 100644 utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java create mode 100644 utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java create mode 100644 utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java create mode 100644 utils/test-connector-remote/README.md create mode 100644 utils/test-connector-remote/build.gradle.kts create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java create mode 100644 utils/test-utils/build.gradle.kts create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0525fc2de --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Compiled class file +*.class + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +.idea +*.iml +.run +.vs +.vscode + +.DS_Store + +**/out +*.hprof + +.env +!launchers/.env + +# Log files +*.log + +**/*.key +**/*.p12 +**/*.jks + +docs/secrets diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2439c0ec6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# This file is for unifying the coding style for different editors and IDEs +# See editorconfig.org +root = true + +[*] +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env b/.env new file mode 100644 index 000000000..a0d3c036f --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# Env variables for docker-compose.yaml +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.4.2 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:7.4.2 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 +EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/.env.dev b/.env.dev new file mode 100644 index 000000000..8c017456b --- /dev/null +++ b/.env.dev @@ -0,0 +1,6 @@ +# Env variables for docker-compose-dev.yaml +EDC_IMAGE=ghcr.io/sovity/edc-dev:latest +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:latest +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest +EDC_UI_ACTIVE_PROFILE=sovity-open-source + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..8510e87df --- /dev/null +++ b/.gitattributes @@ -0,0 +1,102 @@ +* text=auto eol=lf + +# Web +*.css text diff=css +*.scss text diff=css +*.htm text diff=html +*.html text diff=html +*.properties text eol=lf + +# Exclude files from exporting +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore + +# Scripts +*.bash text eol=lf +*.fish text eol=lf +*.sh text eol=lf + +# Windows Scripts need crlf +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Documents +*.tex text diff=tex +*.bibtex text diff=bibtex +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +# Graphics +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.gifv binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + +# Fonts +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +# Archives +*.7z binary +*.gz binary +*.tar binary +*.tgz binary +*.zip binary + +# Audio +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +# Video +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.ogv binary +*.swc binary +*.swf binary +*.webm binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..93c91f3fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,62 @@ +name: Bug Report Template +description: Report a bug to help us improve +labels: ["kind/bug"] +body: + - type: textarea + id: description + attributes: + label: Description - What happened? * + description: A clear and concise description of the bug. + placeholder: Tell us what you see! + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior * + description: A clear and concise description of what you expected to happen. + placeholder: Tell us what you expected! + validations: + required: true + - type: textarea + id: observed + attributes: + label: Observed Behavior * + description: A clear and concise description of what happened instead. + placeholder: Tell us what you observed! + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior. + placeholder: Tell us how to reproduce the issue! + validations: + required: false + - type: textarea + id: context + attributes: + label: Context Information + description: Add any other context about the problem here. + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots or other information to help explain your problem. + validations: + required: false + - type: markdown + attributes: + value: | + _* These fields are mandatory, without filling them it is not possible to create the issue._ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..0086358db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 000000000..4ca8c166d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,30 @@ +--- +name: Documentation Update Request +about: Create a report to help us improve our documentation +title: "" +labels: "task/documentation" +assignees: "" +--- + +# Documentation Update Request + +## Description + + +## Current Documentation + + +## Proposed Changes + + +## Justification + + +## Additional Context + + +## Deadline + + +## Notes + diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 000000000..16055ebc3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,27 @@ +--- +name: Enhancement +about: Implement a task / improve something +title: "" +labels: ["kind/enhancement", "scope/ce"] +assignees: "" +--- + +# Enhancement + +## Description + +_A clear and concise description of what the customer wants to happen._ + +- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. + +## Stakeholders + +_Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ + +## Solution Proposal and Work Breakdown + +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine a Solution Proposal / Work Breakdown +``` + diff --git a/.github/ISSUE_TEMPLATE/epic_template.md b/.github/ISSUE_TEMPLATE/epic_template.md new file mode 100644 index 000000000..24edb0b59 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic_template.md @@ -0,0 +1,41 @@ +--- +name: Epic +about: Help us with new ideas +title: "" +labels: "kind/epic" +assignees: "" +--- + +# Epic + +## Description + + +### Requirements + + +## Work Breakdown + + +```[tasklist] +### Stories +- [ ] Create Stories which can be converted into issues +``` + +## Initiative / goal + + +### Hypothesis + + +## Acceptance criteria and must have scope + + +## Stakeholders + + +## Timeline + + +## Need for refinement + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..5ff7afa21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,34 @@ +--- +name: Feature Request +about: Help us with new features +title: "" +labels: "kind/enhancement" +assignees: "" +--- + +# Feature Request + +## Description + +- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. + +## Which Areas Would Be Affected? + + +## Why Is the Feature Desired? + + +## How does this tie into our current product? + + +## Stakeholders + + +## Solution Proposal and Work Breakdown + + +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine a Solution Proposal / Work Breakdown +- [ ] (For Tech Team): Include acceptance criteria for the sub-tasks of the work breakdown +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request_mds.md b/.github/ISSUE_TEMPLATE/feature_request_mds.md new file mode 100644 index 000000000..510c0cb95 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_mds.md @@ -0,0 +1,39 @@ +--- +name: Feature Request (MDS) +about: Help us improve the MDS CE EDC Connector product experience with new features suggestions +title: "" +labels: ["kind/enhancement", "task/analyze", "status/blocked/needs-product", "scope/mds", "scope/ce"] +assignees: ["jkbquabeck", "AbdullahMuk"] +--- + +# Feature Request + +## Description + +_A clear and concise description of what the customer wants to happen._ + +- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. + +## Which Areas Would Be Affected? + +_e.g., DPF, CI, build, transfer, etc._ + +## Why Is the Feature Desired? + +_What problems does that user face that existing functionalities do solve?_ + +## How does this tie into the current product? + +_Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new innovative ideas?_ + +## (For sovity Team to complete) Stakeholders + +_Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ + +## (For sovity Team to complete) Solution Proposal and Work Breakdown + +```[tasklist] +- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata +- [ ] Refine further action items for this feature request +``` + diff --git a/.github/ISSUE_TEMPLATE/process.md b/.github/ISSUE_TEMPLATE/process.md new file mode 100644 index 000000000..4957c23cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/process.md @@ -0,0 +1,24 @@ +--- +name: Refine Process Request +about: Existing processes must be adapted or new ones created +title: "" +labels: "task/documentation" +assignees: "" +--- + +# Process Refinement Request + +## Description + + +## Current State + + +## Proposed Changes + + +## Related Issues or PRs + + +## Additional Information + diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md new file mode 100644 index 000000000..e6d651e53 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.md @@ -0,0 +1,72 @@ +--- +name: Release +about: Create an issue to track a release process. +title: "Release x.x.x" +labels: ["task/release", "scope/ce"] +assignees: "" +--- + +# Release + +## Work Breakdown + +Feel free to edit this release checklist in-progress depending on what tasks need to be done: + +- [ ] Release [edc-ui](https://github.com/sovity/edc-ui), this might require several steps, first of which is to [create a new `Release` issue](https://github.com/sovity/edc-ui/issues/new/choose) +- [ ] Decide a release version depending on major/minor/patch changes in the CHANGELOG.md. +- [ ] Update this issue's title to the new version +- [ ] `release-prep` PR: + - [ ] Write or review the current [Productive Deployment Guide](https://github.com/sovity/edc-extensions/blob/main/docs/deployment-guide/goals/production) + - [ ] Write or review the current [Development Deployment Guide](https://github.com/sovity/edc-extensions/blob/main/docs/deployment-guide/goals/development) + - [ ] Write or review the current [Local Demo Deployment Guide](https://github.com/sovity/edc-extensions/blob/main/docs/deployment-guide/goals/local-demo) + - [ ] For Major version updates: If we want to continue supporting the old major version: + - [ ] Keep the old Productive Development Guide in a separate location. + - [ ] Add a note to the old version about its deprecation status. + - [ ] Add a Link the old version in the new version for discoverability. + - [ ] Check all links in the old version. + - [ ] Keep the old Productive Development Guide in a separate location. + - [ ] Add a note to the old version about its deprecation status. + - [ ] Add a Link the old version in the new version for discoverability. + - [ ] Check all links in the old version. + - [ ] Update the CHANGELOG.md. + - [ ] Add a clean `Unreleased` version. + - [ ] Add the version to the old section. + - [ ] Add the current date to the old version. + - [ ] Check the commit history for commits that might be product-relevant and thus should be added to the + changelog. Maybe they were forgotten. + - [ ] Write or review the `Deployment Migration Notes` section, check the commit history for changed / added + configuration properties. + - [ ] Write or review a release summary. + - [ ] Write or review the compatible versions section. + - [ ] Add a link to the EDC UI Release to the "EDC UI" section. + - [ ] Add a link to the EDC UI Release Deployment Migration Notes from the Deployment Migration section if the EDC UI has Deployment Migration Notes. + - [ ] Remove empty sections from the patch notes. + - [ ] Replace the existing `docker-compose.yaml` with `docker-compose-dev.yaml`. + - [ ] Set the version for `EDC_IMAGE` of + the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + - [ ] Set the version for `TEST_BACKEND_IMAGE` of + the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + - [ ] Set the UI release version for `EDC_UI_IMAGE` of + the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + - [ ] If the Eclipse EDC version changed, update + the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-extensions/blob/main/docs/eclipse-edc-management-api.yaml). + - [ ] Update the Postman Collection if required. + - [ ] Merge the `release-prep` PR. +- [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-extensions/actions). +- [ ] Validate the image + - [ ] Pull the latest latest edc-dev image: `docker image pull ghcr.io/sovity/edc-dev:latest`. + - [ ] Check that your image was built recently `docker image ls | grep ghcr.io/sovity/edc-dev`. + - [ ] Test the release `docker-compose.yaml` with `EDC_IMAGE=ghcr.io/sovity/edc-dev:latest` (at minimum execute a transfer between the two connectors). + - [ ] Test with `EDC_UI_ACTIVE_PROFILE=sovity-open-source` + - [ ] Test with `EDC_UI_ACTIVE_PROFILE=mds-open-source` + - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. +- [ ] Test the postman collection against that running docker-compose. +- [ ] [Create a release](https://github.com/sovity/edc-extensions/releases/new) + - [ ] In `Choose the tag`, type your new release version in the format `vx.y.z` (for instance `v1.2.3`) then click `+Create new tag vx.y.z on release`. + - [ ] Re-use the changelog section as release description, and the version as title. +- [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). +- [ ] Revisit the changed list of tasks and compare it + with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-extensions/blob/main/.github/ISSUE_TEMPLATE/release.md). + Propose changes where it makes sense. +- [ ] Close this issue. +- [ ] Inform the Product Manager of this new release diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..c777e39aa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +_What issues does this PR close?_ + + +```[tasklist] +### Checklist +- [ ] The PR title is short and expressive. +- [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/authority-portal/tree/main/docs/dev/changelog_updates.md) for more information. +- [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. +- [ ] I have performed a **self-review** +``` diff --git a/.github/actions/build-connector-image/action.yml b/.github/actions/build-connector-image/action.yml new file mode 100644 index 000000000..1dfbefa10 --- /dev/null +++ b/.github/actions/build-connector-image/action.yml @@ -0,0 +1,75 @@ +name: "Build EDC Connector Image" +description: "Builds and deploys the React frontend to AWS S3" +inputs: + registry-url: + required: true + description: "Docker Registry" + registry-user: + required: true + description: "Docker Registry Login Username" + registry-password: + required: true + description: "Docker Registry Login Password" + image-base-name: + required: true + description: "Docker Image Base Name (Company)" + image-name: + required: true + description: "Docker Image Name (Artifact Name)" + connector-name: + required: true + description: "EDC Connector Name in launchers/connectors/{connector-name}" + title: + required: true + description: "Docker Image Title" + description: + required: true + description: "Docker Image Description" +runs: + using: "composite" + steps: + - name: "Docker: Log in to the Container registry" + uses: docker/login-action@v2 + with: + registry: ${{ inputs.registry-url }} + username: ${{ inputs.registry-user }} + password: ${{ inputs.registry-password }} + - name: "Docker: Store last commit info and build date" + id: last-commit-information + shell: bash + run: | + echo "LAST_COMMIT_INFO<> $GITHUB_ENV + export LAST_COMMIT_INFO=$(git log -1) + echo "$LAST_COMMIT_INFO" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + echo "BUILD_DATE=$(date --utc +%FT%TZ)" >> $GITHUB_ENV + - name: "Docker: Extract metadata (tags, labels)" + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ inputs.registry-url }}/${{ inputs.image-base-name }}/${{ inputs.image-name }} + labels: | + org.opencontainers.image.title=${{ inputs.title }} + org.opencontainers.image.description=${{ inputs.description }} + tags: | + type=schedule + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=release,enable=${{ startsWith(github.ref, 'refs/tags/') }} + - name: "Docker: Build and Push" + uses: docker/build-push-action@v4 + with: + file: launchers/Dockerfile + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + CONNECTOR_NAME=${{ inputs.connector-name }} + "EDC_LAST_COMMIT_INFO_ARG=${{ env.LAST_COMMIT_INFO }}" + EDC_BUILD_DATE_ARG=${{ env.BUILD_DATE }} diff --git a/.github/markdown-link-checker-config.jq b/.github/markdown-link-checker-config.jq new file mode 100755 index 000000000..8491117da --- /dev/null +++ b/.github/markdown-link-checker-config.jq @@ -0,0 +1,21 @@ +#!/usr/bin/env -S jq -nf +{ + "ignorePatterns": [ + {"pattern": "^https?://localhost"}, + {"pattern": "^https?://example"}, + {"pattern": "^https://checkstyle\\.sourceforge\\.io"}, + {"pattern": "^https://www\\.linkedin\\.com"}, + {"pattern": "https://(.*?)\\.azure\\.sovity\\.io"}, + {"pattern": "http://edc2?:"} + ], + "replacementPatterns": [ + { + "pattern": "^https://github.com/sovity/edc-extensions/blob/main/", + "replacement": "https://github.com/sovity/edc-extensions/blob/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" + }, + { + "pattern": "^https://github.com/sovity/edc-extensions/tree/main/", + "replacement": "https://github.com/sovity/edc-extensions/tree/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" + } + ] +} diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml new file mode 100644 index 000000000..76fd8814c --- /dev/null +++ b/.github/workflows/add_issue_to_project.yml @@ -0,0 +1,16 @@ +name: Add issue to project action + +on: + issues: + types: + - opened + +jobs: + add_issue_to_project: + name: add_issue_to_project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1.0.1 + with: + project-url: https://github.com/orgs/sovity/projects/9 + github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..b6215677c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,168 @@ +name: CI + +on: + push: + branches: [ main ] + release: + types: [ published ] + pull_request: + branches: [ main ] + +env: + REGISTRY_URL: ghcr.io + REGISTRY_USER: ${{ github.actor }} + IMAGE_BASE_NAME: ${{ github.repository_owner }} + GITHUB_CI: true + +jobs: + build-gradle-project: + name: Build Gradle Project + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + steps: + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: actions/checkout@v3 + - name: "Set up JDK 17" + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: "Gradle: Validate Gradle Wrapper" + uses: gradle/wrapper-validation-action@ccb4328a959376b642e027874838f60f8e596de3 + - name: "Gradle: Include last commit info and build date for JARs" + run: | + git log -1 > extensions/last-commit-info/src/main/resources/jar-last-commit-info.txt + echo $(date --utc +%FT%TZ) > extensions/last-commit-info/src/main/resources/jar-build-date.txt + - name: "Gradle: Overwrite Artifact Version (Release Only)" + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + GRADLE_ARGS="-PsovityEdcExtensionsVersion=${GITHUB_REF#refs/tags/v}" + echo "GRADLE_ARGS=$GRADLE_ARGS" >> $GITHUB_ENV + - name: "Gradle: Build" + uses: gradle/gradle-build-action@v2.10.0 + with: + arguments: build ${{ env.GRADLE_ARGS }} + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Gradle: Publish (Main & Release Only)" + uses: gradle/gradle-build-action@v2.10.0 + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + with: + arguments: publish ${{ env.GRADLE_ARGS }} + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Docker Image: edc-dev" + uses: ./.github/actions/build-connector-image + with: + registry-url: ${{ env.REGISTRY_URL }} + registry-user: ${{ env.REGISTRY_USER }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + image-base-name: ${{ env.IMAGE_BASE_NAME }} + image-name: "edc-dev" + connector-name: "sovity-dev" + title: "sovity Dev EDC Connector" + description: "Extended EDC Connector built by sovity. This dev version contains no dataspace auth and can be used to quickly start a locally running EDC + EDC UI." + - name: "Docker Image: edc-ce" + uses: ./.github/actions/build-connector-image + with: + registry-url: ${{ env.REGISTRY_URL }} + registry-user: ${{ env.REGISTRY_USER }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + image-base-name: ${{ env.IMAGE_BASE_NAME }} + image-name: "edc-ce" + connector-name: "sovity-ce" + title: "sovity Community Edition EDC Connector" + description: "EDC Connector built by sovity. Contains sovity's Community Edition EDC extensions and requires dataspace credentials to join an existing dataspace." + - name: "Docker Image: edc-ce-mds" + uses: ./.github/actions/build-connector-image + with: + registry-url: ${{ env.REGISTRY_URL }} + registry-user: ${{ env.REGISTRY_USER }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + image-base-name: ${{ env.IMAGE_BASE_NAME }} + image-name: "edc-ce-mds" + connector-name: "mds-ce" + title: "MDS Community Edition EDC Connector" + description: "EDC Connector built by sovity and configured for compatibility with the Mobility Data Space (MDS). This EDC requires dataspace credentials, and additional MDS Services such as a Clearing House." + - name: "Docker Image: test-backend" + uses: ./.github/actions/build-connector-image + with: + registry-url: ${{ env.REGISTRY_URL }} + registry-user: ${{ env.REGISTRY_USER }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + image-base-name: ${{ env.IMAGE_BASE_NAME }} + image-name: "test-backend" + connector-name: "test-backend" + title: "Test Data Source / Data Sink" + description: "Provides a minimal data source / data sink for E2E tests." + ts-api-client-library: + name: TS API Client Library + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: actions/checkout@v3 + - name: "Set up JDK 17" + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: "Set up Node 16" + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'npm' + cache-dependency-path: extensions/wrapper/clients/typescript-client/package.json + - name: "Gradle: Validate Gradle Wrapper" + uses: gradle/wrapper-validation-action@ccb4328a959376b642e027874838f60f8e596de3 + - name: "Gradle: Generate TS Code" + uses: gradle/gradle-build-action@v2.10.0 + with: + arguments: :extensions:wrapper:wrapper:build -x test + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "NPM: Dist Tag & Version" + working-directory: ./extensions/wrapper/clients/typescript-client + run: | + if [[ "$GITHUB_REF" == "refs/tags/v"* ]]; then + # Full Release + VERSION="${GITHUB_REF#refs/tags/v}" + DIST_TAG=latest + else + VERSION="0.$(date '+%Y%m%d.%H%M%S')-main-$CI_SHA_SHORT" + DIST_TAG=main + fi + npm version $VERSION + echo "DIST_TAG=$DIST_TAG" >> $GITHUB_ENV + - name: "NPM: Build" + working-directory: extensions/wrapper/clients/typescript-client + run: npm ci && npm run build + - name: "NPM: Publish (Main & Releases Only)" + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + working-directory: extensions/wrapper/clients/typescript-client + run: | + npm set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN + npm set //registry.npmjs.org/:username $NODE_USER + npm publish --access public --tag "${{ env.DIST_TAG }}" + env: + NODE_USER: richardtreier-sovity + NODE_AUTH_TOKEN: ${{ secrets.SOVITY_EDC_CLIENT_NPM_AUTH }} + markdown-link-checks: + name: Markdown Link Checks + runs-on: ubuntu-latest + steps: + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: actions/checkout@master + - name: "Markdown Link Checker: Generate Config" + run: .github/markdown-link-checker-config.jq > .github/markdown-link-checker-config.json + - name: "Markdown Link Checker: Validate Links" + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-quiet-mode: 'yes' + config-file: '.github/markdown-link-checker-config.json' diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml new file mode 100644 index 000000000..0e9cab29b --- /dev/null +++ b/.github/workflows/code_analysis.yml @@ -0,0 +1,59 @@ +name: Code Analysis + +on: + workflow_dispatch: + pull_request: + branches: [main] + paths-ignore: + - "**.md" + - "docs/**" + +jobs: + is_java_project: + runs-on: ubuntu-latest + outputs: + pom_exists: ${{ steps.check_files.outputs.files_exists }} + checkstyle_active: ${{ steps.check_checkstyle.outputs.checkstyle_active }} + spotbugs_active: ${{ steps.check_spotbugs.outputs.spotbugs_active }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Check file existence + id: check_files + uses: andstor/file-existence-action@v2 + with: + files: "pom.xml" + - name: check_checkstyle + id: check_checkstyle + run: echo "checkstyle_active=$(if grep -q "maven-checkstyle-plugin" pom.xml; then echo "true"; else echo "false"; fi)" >> $GITHUB_OUTPUT + - name: check_spotbugs + id: check_spotbugs + run: echo "spotbugs_active=$(if grep -q "spotbugs-maven-plugin" pom.xml; then echo "true"; else echo "false"; fi)" >> $GITHUB_OUTPUT + run_checkstyle: + needs: [is_java_project] + if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.checkstyle_active == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + cache: "maven" + - name: Run style checks + run: mvn -B checkstyle:check --file pom.xml + run_spotbugs: + needs: [is_java_project] + if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.spotbugs_active == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + cache: "maven" + - name: Run static code analysis + run: mvn -B compile spotbugs:check --file pom.xml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..868d110a5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,49 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '34 8 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java-kotlin', 'javascript-typescript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/license_scan.yml b/.github/workflows/license_scan.yml new file mode 100644 index 000000000..ebd597ac6 --- /dev/null +++ b/.github/workflows/license_scan.yml @@ -0,0 +1,41 @@ +name: Trivy License Scan + +on: + push: + +jobs: + license_scan1: + name: License scan (rootfs) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run license scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "rootfs" + scan-ref: "." + scanners: "license" + severity: "CRITICAL,HIGH" + exit-code: 1 + license_scan2: + name: License scan (repo) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: npm install (typescript-client) + run: cd extensions/wrapper/clients/typescript-client && npm install + - name: npm install (typescript-client-example) + run: cd extensions/wrapper/clients/typescript-client-example && npm install + - name: Run license scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "repo" + scan-ref: "." + scanners: "license" + severity: "CRITICAL,HIGH" + exit-code: 1 diff --git a/.github/workflows/secret_scan.yml b/.github/workflows/secret_scan.yml new file mode 100644 index 000000000..b27e1f6b1 --- /dev/null +++ b/.github/workflows/secret_scan.yml @@ -0,0 +1,25 @@ +name: Trivy Secret Scan + +on: + push: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + secret-scan: + name: secret_scan + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Run vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + exit-code: "1" + ignore-unfixed: true + scanners: secret diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml new file mode 100644 index 000000000..6a5076180 --- /dev/null +++ b/.github/workflows/security_scan.yml @@ -0,0 +1,43 @@ +name: Trivy Security Scan + +on: + push: + workflow_dispatch: + +jobs: + security_scan_rootfs: + name: security_scan_rootfs + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run static analysis (rootfs) + uses: aquasecurity/trivy-action@master + with: + scan-type: "rootfs" + scanners: "vuln,misconfig" + ignore-unfixed: true + format: "sarif" + output: "trivy-results-rootfs.sarif" + severity: "CRITICAL,HIGH" + security_scan_repo: + name: security_scan_repo + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run static analysis (repo) + uses: aquasecurity/trivy-action@master + with: + scan-type: "repo" + scanners: "vuln,misconfig" + ignore-unfixed: true + format: "sarif" + output: "trivy-results-repo.sarif" + severity: "CRITICAL,HIGH" + - name: Upload Trivy scan results to GitHub Security tab (repo) + uses: github/codeql-action/upload-sarif@v2 + continue-on-error: true + with: + sarif_file: "trivy-results-repo.sarif" + category: "code" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 000000000..1a415400c --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,32 @@ +name: Trivy Security Scans + +on: + pull_request: + branches: [ "main" ] + + +jobs: + build: + name: build + runs-on: ubuntu-20.04 + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run static analysis + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + security-checks: 'vuln,secret,config' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL' + + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + category: 'code' diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..81df1bf93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Compiled class file +*.class + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +.idea +*.iml +.run +.vs +.vscode + +.DS_Store + +**/out +*.hprof + +**/.env +!.env +!launchers/.env + +# Log files +*.log + +**/*.key +**/*.p12 +**/*.jks diff --git a/.pre-commit-README.md b/.pre-commit-README.md new file mode 100644 index 000000000..1ee954fff --- /dev/null +++ b/.pre-commit-README.md @@ -0,0 +1,25 @@ +# Pre-Commit-Hook +The defined pre-commit-hook prevents committing passwords to the repository. In case a password is detected +git commit fails. + +## Install pre-commit and detect-secrets +1. Install pre-commit-hook tool + `$ pip install pre-commit` +2. Install detect-secrets + `$ pip install detect-secrets` + +## Enable secret-scanning pre-commit hook +1. Update pre-commit-hook + `$ pre-commit autoupdate` +2. Enable defined pre-commit-hook + `$ pre-commit install` + +## On repository initialization of pre-commit hook with detect-secrets +If no `.secrets.baseline` is present, simply generate it: +1. `$ detect-secrets scan --disable-plugin KeywordDetector --disable-plugin AWSKeyDetector > .secrets.baseline` +2. Use Notepad++ or IntelliJ-Editor to convert `.secrets.baseline` to UTF-8 + +## Add false-positives or force adding secrets +1. `$ detect-secrets scan --baseline .secrets.baseline` +2. If secrets are identified, add them to .secrets.baseline manually +For more details see: https://github.com/Yelp/detect-secrets#adding-secrets-to-baseline diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..5d62bf09d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + exclude: package.lock.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a157402dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,743 @@ +# Changelog + +For documentation on how to update this changelog, +please see [changelog_updates.md](docs/dev/changelog_updates.md). + +## [x.x.x] - UNRELEASED + +### Overview + +### EDC UI + +### EDC Extensions + +#### Major Changes + +#### Minor Changes + +#### Patch Changes + +### Deployment Migration Notes + +## [7.4.2] - 2024-04-20 + +### Overview + +MDS Bugfix Release + +### Detailed Changes + +#### Patch Changes + +- Fixed a bug causing Catalog fetches to fail if a data offer with an empty DataModel value existed. +- Fixed naming of the `nutsLocations` field for MDS assets. +- UI: Removed HTTP Verb "HEAD" as it was not supported by the backend +- Docs: Updated image to explain data-transfer-methods +- Docs: Updated documentation for parameterization using [only the UI](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/parameterized_assets_via_ui.md) or the [Management-API](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/parameterized_assets.md) +- Docs: Updated [OAuth2 documentation](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/oauth-data-address.md) about necessary parameters that need to use the vault key instead of providing a secret directly +- Docs: Updated documentation for the [pull-data-transfer](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/pull-data-transfer.md) +- Dev Utils: Parallel test support for our Test Backend for some requests. + +### Deployment Migration Notes + +Contains DB migrations, DB backups advised. + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.4.2` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.4.2` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.4.2` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` +- Connector UI Release: https://github.com/sovity/edc-ui/releases/tag/v3.2.2 + +## [7.4.0] - 2024-04-11 + +### Overview + +MDS bugfixes. + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v3.1.0 + +#### Minor Changes + +- Logginghouse-Client: Add logging-house-client extension 0.2.10 +- Migrated MDS fields to mobilityDCAT-AP +- Added a workaround for the assets' parameterization using a fork of the Eclipse EDC 0.2.1 + +### Deployment Migration Notes + +- A new LoggingHouse extension is now included in the EDC CE MDS variant, which means that additional properties must be set for it: + - `EDC_LOGGINGHOUSE_EXTENSION_ENABLED: "true"` + - `EDC_LOGGINGHOUSE_EXTENSION_URL: #LoggingHouse URL of the MDS environment` + +[EDC UI Migration Notes](https://github.com/sovity/edc-ui/blob/v3.1.0/CHANGELOG.md#v310---2024-04-11) + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.4.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.4.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.4.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.1.0` + +## [7.3.0] - 2024-03-28 + +### Overview + +Some API Wrapper improvements, some bugfixes. + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v3.0.0 + +### EDC Extensions + +#### Minor Changes + +- UIAsset: Replaced unsafe additional and private properties with safer alternative fields `customJsonAsString` (**not** affected by Json LD manipulation) and `customJsonLdAsString` (affected by Json LD manipulation), along with their private counterparts. +- API Wrapper: TS Client Library now supports OAuth Client Credentials +- EDC Backend: Added config variables for remote debugging + +#### Patch Changes + +- Add a fix for a null pointer exception in the transfer history API. +- Add e2e test for double encoding of query parameters + +### Deployment Migration Notes + +- EDC UI: + - New **optional** environment variable: `EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD` as override for shown Management API URL on the dashboard +- EDC Backend: + - New **optional** environment variables to enable and configure remote logging & debugging capabilities: + - `DEBUG_LOGGING = false` + - `REMOTE_DEBUG = false` + - `REMOTE_DEBUG_SUSPEND = false` + - `REMOTE_DEBUG_BIND = 127.0.0.1:5005` + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.3.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.3.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.3.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.0.0` + +## [7.2.2] - 2024-03-13 + +### Overview + +Bugfix + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.4.0 + +### EDC Extensions + +#### Patch Changes + +- DspCatalogService: Stable Contract Offer IDs removed + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.2.2` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.2.2` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.2.2` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` + +## [7.2.1] - 2024-02-21 + +### Overview + +Bugfixes + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.4.0 + +### EDC Extensions + +#### Patch Changes + +- DspCatalogService: Contract Offer IDs are now stable +- Fixed some requests' timeouts by removing the data-plane-instance-store-sql Extension + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.2.1` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.2.1` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.2.1` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` + +## [7.2.0] - 2024-02-14 + +### Overview + +MDS bugfix and feature release + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.4.0 + +#### Minor Changes + +- Assets now have new MDS fields + +#### Patch Changes + +- Docs: Improved documentation of HTTP pull (edc-ui) +- Docs: Add security recommendations for recent API key vulnerabilities +- Fixed connector restricted usage policy +- Fixed connection pool issues by switching to Tractus-X connection pool + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.2.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.2.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.2.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` + +## [7.1.1] - 2024-01-18 + +### Overview + +Bugfix release for minor UI bugs + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.3.1 + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.1.1` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.1.1` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.1.1` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.3.1` + +## [7.1.0] - 2024-01-17 + +### Overview + +MDS feature release: Asset markdown descriptions and editable metadata + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.3.0 + +### EDC Extensions + +#### Minor Changes + +- Asset metadata is now editable +- Asset descriptions now support Markdown +- Negotiate button is no longer shown for own connector endpoints + +### Deployment Migration Notes + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.1.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.1.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.1.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.3.0` + +## [7.0.0] - 2023-12-06 + +### Overview + +`MY_EDC_PARTICIPANT_ID` must now coincide with a DAT claim. +This fixes the Contract Negotiation issue that affected `5.0.0` and `6.0.0`. + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.2.0 + +### EDC Extensions + +#### Major Changes + +- Participant IDs must now coincide with a DAT claim. + +#### Patch Changes + +- Fixed an issue preventing Contract Negotiations. +- Fixed an issue preventing transfer processes from being marked as `COMPLETED` in Eclipse EDC `0.2`. +- Fixed policy and permission targets shown as warnings in the UI. +- Added example for using the API Wrapper to offer and consume data. +- Added CHANGELOG documentation. +- Marked `MY_EDC_NAME_KEBAB_CASE` as deprecated in favor of `MY_EDC_PARTICIPANT_ID`. + +### Deployment Migration Notes + +- The configured value of `MY_EDC_PARTICIPANT_ID` will now be validated via the DAPS: + - The configured value of `MY_EDC_PARTICIPANT_ID` must coincide with the claim value `referringConnector` + as configured for this Connector in the DAPS. + - For MS8-migrated connectors, if the Participant ID was not configured well before, existing contract agreements + will stop working. The Participant ID is referenced heavily in counter-party connectors, which makes a migration + of Participant IDs for old contract agreements impractical. +- If a given data space has no "Participant ID" / "Connector ID" concept or does not use the `referringConnector` claim: + - It is possible to override the checked claim by overriding `EDC_AGENT_IDENTITY_KEY`. + - `EDC_AGENT_IDENTITY_KEY` could be set to the claim name of the AKI / SKI Client ID, which should always be part of + the issued DAT. This would be `sub` for a sovity DAPS and `client_id` for an Omejdn DAPS. + - `MY_EDC_PARTICIPANT_ID` would have to be set to the AKI / SKI Client ID. +- Renamed ~~`MY_EDC_NAME_KEBAB_CASE`~~ to `MY_EDC_PARTICIPANT_ID`. ~~`MY_EDC_NAME_KEBAB_CASE`~~ continues working, but + prints a warning on startup if configured. + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.2.0` + +## [6.0.0] - 2023-11-17 + +### Overview + +Connectors are now pre-configured for the sovity DAPS over Omejdn. + +This fixes issues with MDS Connectors not being able to connect to the MDS 2.0. + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.1.0 + +### EDC Extensions + +#### Major Changes + +- The default DAPS configuration now supports the sovity DAPS over Omejdn. + +#### Patch Changes + +- Improved `:extensions:wrapper:wrapper-common-mappers` for broker: `AssetJsonLdUtils`, made some methods public. +- Added example for using the API Wrapper to offer and consume data. + +### Deployment Migration Notes + +Omejdn DAPS users need to manually add the following Backend ENV Vars: + +```yaml +EDC_OAUTH_PROVIDER_AUDIENCE: idsc:IDS_CONNECTORS_ALL +EDC_OAUTH_ENDPOINT_AUDIENCE: idsc:IDS_CONNECTORS_ALL +EDC_AGENT_IDENTITY_KEY: client_id +``` + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:6.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:6.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:6.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` + +## [5.0.0] - 10.10.2023 + +### Overview + +Migration from Eclipse EDC Milestone 8 to Eclipse EDC 0.2.1. + +The API Wrapper and API Client Libraries can now be used to fully control a sovity EDC Connector. + +### EDC UI + +https://github.com/sovity/edc-ui/releases/tag/v2.0.0 + +### EDC Extensions + +#### Major Changes + +- Bump Eclipse EDC Version to `0.2.1`: + - Now using the Data Space Protocol (DSP) over the ~~IDS Protocol~~. + - Major changes to the Management API. See the postman collection / OpenAPI file. +- The Getting Started Docker Compose file is no longer to be used as reference for deployments: + - The Getting Started Docker Compose file now launches connectors for local demo purposes. + - For productive deployments, a detailed deployment guide has been added. + - The Dev-Images now also require a PostgreSQL Database. +- Removed IDS Broker Extension. +- Removed IDS Clearing House Extension. + +#### Minor Changes + +- All Connector UI Endpoints were migrated to our UI API Wrapper. New UI API Wrapper Endpoints: + - Asset Page + - Create Asset + - Delete Asset + - Catalog / Data Offers + - Contract Definition Page + - Contract Negotiation Start / Detail + - Create Contract Definition + - Delete Contract Definition + - Policy Definition Page + - Create Policy Definition + - Delete Policy Definition + - Dashboard Page +- New modules with common UI models and mappers for the Connector UI and Broker UI: `:extensions:wrapper:wrapper-common-api` and `:extensions:wrapper:wrapper-common-mappers`. +- New module with centralized Vocab and utilities for dealing with EDC / DCAT JSON-LD: `:utils:json-and-jsonld-utils` +- New module with utilities for parsing DCAT Catalog responses for use in the UI API Wrapper and the Broker Server: `:utils:catalog-parser` +- New modules with utilities for E2E Testing Connectors: `:utils:test-connector-remote` and `:extensions:test-backend-controller` + +#### Patch Changes + +- New modules in `:launchers:common` and `:launchers:connectors` so building different variants no longer requires separate builds. +- New module `:extensions:wrapper:wrapper-api` split from `:extensions:wrapper:wrapper` so integration tests in `wrapper` can use the Java Client Library. +- New JUnit E2E Tests in `:launchers:connectors:sovity-dev` that start two connectors and test the data exchange. + +### Deployment Migration Notes + +1. Deployment Migration Notes for the EDC UI: https://github.com/sovity/edc-ui/releases/tag/v2.0.0 +2. The Connector Endpoint changed to `https://[FQDN]/api/dsp` from ~~`https://[FQDN]/api/v1/ids/data`~~. +3. The Management Endpoint changed to `https://[FQDN]/api/management` from ~~`https://[FQDN]/api/v1/management`~~. +4. The `v1` Eclipse EDC Management API has been replaced by the Eclipse EDC `JSON-LD` `v2` Management API. Our Postman Collection shows some example requests. + However, a switch to our [API Wrapper](extensions/wrapper/README.md) is recommended. Despite our Use Case API Wrapper API still being in development, + the Connector UI API Wrapper is fully functional and can be used in concatenation with our type-safe generated API Client Libraries to both provide and + consume data offers. +5. The Connector now uses the Data Space Protocol (DSP) instead of the IDS Protocol. This requires different paths to be available from the internet. + Please refer to our deployment guide for more information. +6. If the old protocol endpoint required HTTP communication to pass as a workaround for a certain bug, this should be undone now, + with all protocol endpoints being secured by HTTPS/TLS. + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:5.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:5.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:5.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:2.0.0` + +## [4.2.0] - 2023-09-01 + +### Overview + +MDS 1.2 release using MS8 EDC. + +### EDC UI + +- https://github.com/sovity/edc-ui/releases/tag/v0.0.1-milestone-8-sovity13 + +### Detailed Changes + +#### Patch Changes + +- Fixed issues with Broker Client Extension causing exceptions, because the MDS no longer uses the legacy broker. + +### Deployment Migration Notes + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:4.2.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:4.2.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:4.2.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` + +## [4.1.0] - 2023-07-24 + +### Overview + +Security improvements of container image and enhancements for the `ReferringConnectorValidationExtension`. + +### EDC UI + +- https://github.com/sovity/edc-ui/releases/tag/v0.0.1-milestone-8-sovity12 + +### EDC-Extensions + +#### Minor Changes + +- ReferringConnectorValidationExtension: Added support for comma separated lists of connectors using the EQ operator as well as pure Lists using the IN operator. + +#### Patch Changes + +- Automatically delete old transfer-processes if there are more than 3000 entries in the transfer-process-table +- Change base-image to `eclipse-temurin:17-jre-alpine` +- Run java process with a non-root user + +### Deployment Migration Notes + +- `default` datasource has to be added + - `EDC_DATASOURCE_DEFAULT_NAME`=default + - `EDC_DATASOURCE_DEFAULT_URL`=jdbc:postgresql://connector:5432/edc + - `EDC_DATASOURCE_DEFAULT_USER`=user + - `EDC_DATASOURCE_DEFAULT_PASSWORD`=password + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:4.1.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:4.1.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:4.1.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` + +## [4.0.1] - 2023-07-07 + +### Overview + +Bugfixes regarding Parameterized Http Datasource Support and open-ended date intervals. + +### EDC UI + +- https://github.com/sovity/edc-ui/releases/tag/v0.0.1-milestone-8-sovity11 + +### EDC-Extensions + +#### Patch Changes + +- Bumped EDC UI Version + +### Deployment Migration Notes + +No changes besides docker image versions. + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:4.0.1` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:4.0.1` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:4.0.1` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity11` + +## [4.0.0] - 2023-07-05 + +### Overview + +Parameterized Http Datasource Support and open-ended date intervals. + +### EDC UI + +- https://github.com/sovity/edc-ui/releases/tag/v0.0.1-milestone-8-sovity9 + +### EDC-Extensions + +#### Major Changes + +- Removed Contract Agreement Transfer API Extension in favor of new API Wrapper UI Endpoint. +- Removed Broker-Server APIs. + +#### Minor Changes + +- UI API: Added support for parameterized HTTP Data Sources. +- Broker-/ClearingHouse-Client: The extensions can be dynamically enabled and disabled via properties (see + getting-started Readme FAQ section). +- Broker Server API: New API Endpoint `DataOfferDetailPage` and `ConnectorDetailPage` with model. + +### Deployment Migration Notes + +No changes besides docker image versions. + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:4.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:4.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:4.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity9` + +## [3.3.0] - 2023-06-06 + +### Minor Changes + +- Added build dates to Last Commit Info Extension +- Added Transfer History Page model to API Wrapper. +- Finalize Broker Server API for PoC. + +### Patch Changes + +- Minor EE API adjustments. + +## [3.2.0] - 2023-05-17 + +### Minor Changes + +- API Wrapper now supports OAuth2 Client Credentials Auth. +- API Wrapper now contains initial Broker Server API Spec. +- API Wrapper now contains initial File Storage Enterprise Edition API Spec. +- API Wrapper Contract Agreement Page Cards now contain Contract Negotiation IDs. + +### Patch Changes + +- Bumped EDC UI version to `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity5` in production `docker-compose.yaml`. This + fixes a CORS-related issue. + +## [3.1.0] - 2023-04-27 + +### Minor Changes + +- feat: wrapper contract agreement api + +### Patch Changes + +- wrapper: added contractAgreements- and transferProcessesCounts +- fix: broker extension provides empty fields +- fix: update postman collection +- bump org.junit.jupiter:junit-jupiter-api from 5.9.2 to 5.9.3 + +## [3.0.1] - 2023-04-06 + +### Fixed + +- Wrong image tag in env file +- `EDC_IDS_ENDPOINT` was not set correctly on image build + +### Changed + +- Reverted docker-compose.yaml to run only one connector + +## [3.0.0] - 2023-04-04 + +### Major Changes + +- Changed EDC Docker Image Variants to `edc-dev`, `edc-ce` and `edc-ce-mds`. +- Changed Java Maven Artifact GroupIds to `de.sovity.edc.ext` and `de.sovity.edc` +- Renamed `broker` to `ids-broker-client`. +- Renamed `clearinghouse` to `ids-clearinghouse-client`. + +### Minor Changes + +- EDC API Wrapper + EDC API Client Bootstrap +- Added Docker Image Tag `release` for latest releases. +- Added sovity Minimal Extension Package. +- broker-extension: Re-register assets at broker at connector startup + +### Patch Changes + +- Reworked Project, Docker Image and Extension documentations. +- broker-extension: Re-register assets at broker at connector startup +- broker-extension: Added a subsequent resource-id filtering after sparql query, to filter out resources that do not + belong to the connector. +- bump org.openapi.generator from 6.3.0 to 6.5.0 +- bump io.quarkus from 2.16.4.Final to 2.16.6.Final +- bump io.quarkus.platform:quarkus-bom from 2.16.5.Final to 2.16.6.Final +- bump io.swagger.core.v3.swagger-gradle-plugin +- bump io.swagger.core.v3:swagger-annotations-jakarta +- bump io.swagger:swagger-annotations from 1.6.8 to 1.6.10 + +## [2.0.3] - 2023-03-24 + +### Fixed + +- Bug in postman collection, ports needed to be updated due to release 2.0.2. + +## [2.0.2] - 2023-03-23 + +### Fixed + +- Bug in migration scripts, for existing contract negotiations the embedded JSON array of contract offers was missing + contractStart, contractEnd. + +## [2.0.1] - 2023-03-21 + +### Fixed + +- Bug in migration scripts, default values are now set. + +## [2.0.0] - 2023-03-20 + +### Fixed + +- Missing blacklist entry for referring connector policy in + docker-compose `POLICY_BROKER_BLACKLIST: REFERRING_CONNECTOR` + +### Changed + +- Updated to EDC-Connector 0.0.1-milestone-8. + +## [1.5.1] - 2023-03-17 + +### Fixed + +- Changed docker-compose file to use released instead of latest versions of EDC-Connector and EDC-UI + +## [1.5.0] - 2023-03-07 + +### Feature + +- `EDC_FLYWAY_REPAIR=true` variable can now be set to run flyway repair when migrations failed + +## [1.4.0] - 2023-03-06 + +### Feature + +- EDC UI Config Extension + +## [1.3.0] - 2023-02-27 + +### Feature: + +- Last Commit Info Extension +- Persistence into PostgreSQL Database + +### Fixed: + +- add if-else switch to get_client.sh for AKI `keyid` keyword +- Set _test_ as default MDS environment (in docs and docker-compose) +- Updated ports of Postman collection json file +- Added unregister connector to puml diagram +- Cannot fetch own catalog due to wrong port mapping + +## [1.2.0] - 2023-02-02 + +### Feature: + +- Add setting `POLICY_BROKER_BLACKLIST` to blacklist policies from being published to broker +- ContractAgreementTransferApi-Extension: Providing an endpoint to start a data-transfer for a contract-agreement + +### Changed + +- Extend get_client script to add support for OpenSSL version 3.x + +## [1.1.0] - 2023-01-23 + +### Feature: + +- Add additional meta information to resource payload when publishing to broker +- Add connector description to broker message +- Add time-interval and participant based policies +- Add ClearingHouse Extension + +### Changed + +- Modified module structure to have only one Boker and one ClearingHouse Extension +- Bump junit-jupiter-api from 5.8.1 to 5.9.2 +- Bump junit-jupiter-engine from 5.8.1 to 5.9.2 +- Bump com.github.johnrengelman.shadow from 7.0.0 to 7.1.2 + +## [1.0.1] - 2023-01-11 + +### Fixed: + +- Connector not registering to broker due to null pointer exception +- Set dev as default environment (in docs and docker-compose) + +### Feature: + +- Add ski/aki and jks extraction script + +## [1.0.0] - 2022-12-19 + +- initial release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..cf40d820a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,70 @@ +# Code of Conduct +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), +version 1.4, available at http://contributor-covenant.org/version/1/4. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..6ccc113be --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,162 @@ +# Contributing to the Project + +Thank you for your interest in contributing to this project + +## Table of Contents + +* [Code Of Conduct](#code-of-conduct) +* [How to Contribute](#how-to-contribute) + * [Discuss](#discuss) + * [Create an Issue](#create-an-issue) + * [Submit a Pull Request](#submit-a-pull-request) + * [Report on Flaky Tests](#report-on-flaky-tests) +* [Etiquette for pull requests](#etiquette-for-pull-requests) +* [Contact Us](#contact-us) + +## Code Of Conduct + +See the [Code Of Conduct](CODE_OF_CONDUCT.md). + +## How to Contribute + +### Discuss + +If you want to share an idea to further enhance the project or discuss potential use cases, please feel free to create a +discussion at the `GitHub Discussions page`] +If you feel there is a bug or an issue, contribute to the discussions in `existing issues` +otherwise [create a new issue](#create-an-issue). + +### Create an Issue + +If you have identified a bug or want to formulate a working item that you want to concentrate on, feel free to create a +new issue at our project's corresponding `GitHub Issues page`. Before doing so, please consider searching for +potentially suitable `existing issues`. + +We also +use [GitHub's default label set](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) +extended by custom ones to classify issues and improve findability. + +If an issue appears to cover changes that will have a (huge) impact on the code base and needs to +first be discussed, or if you just have a question regarding the usage of the software, please +create a `discussion` before raising an issue. + +Please note that if an issue covers a topic or the response to a question that may be interesting +for other developers or contributors, or for further discussions, it should be converted to a +discussion and not be closed. + +### Adhere to Coding Style Guide + +We aim for a coherent and consistent code base, thus the coding style detailed in the [styleguide](STYLEGUIDE.md) should +be followed. + +### Submit a Pull Request + +We would appreciate if your pull request applies to the following points: + +* Conform to following [Etiquette for pull requests](#etiquette-for-pull-requests): + +* Make sure to adjust copyright headers appropriately. + +* The git commit messages should comply to the following format: + ``` + (): + ``` + + Use the [imperative mood](https://github.com/git/git/blob/master/Documentation/SubmittingPatches) + as in "Fix bug" or "Add feature" rather than "Fixed bug" or "Added feature" and + [mention the GitHub issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) + e.g. `chore(transfer process): improve logging`. + +* Add meaningful tests to verify your submission acts as expected. + +* Where code is not self-explanatory, add documentation providing extra clarification. + +* PR descriptions should use the current [PR template](.github/PULL_REQUEST_TEMPLATE.md) + +* Submit a draft pull request at early-stage and add people previously working on the same code as + reviewer. Make sure automatic checks pass before marking it as "ready for review": + + * _Continuous Integration_ performing various test conventions. + +### Report on Flaky Tests + +If you discover a randomly failing ("flaky") test, please take the time to check whether an issue for that already +exists and if not, create an issue yourself, providing meaningful description and a link to the failing run. Please also +label it with `Bug` and `github`. Then assign it to whoever was the original author of the relevant piece of code or +whoever worked on it last. If assigning the issue is not possible due to missing rights, please just comment and +@mention the author/last editor. + +Please do not just restart the run, as this would overwrite the results. If you need to, a better way of doing this is +to push an empty commit. This will trigger another run. + +```bash +git commit --allow-empty -m "trigger CI" && git push +``` + +If an issue labeled with `Bug` and `github` is assigned to you, please prioritize addressing this issue as other +people will be affected. +We are taking the quality of our code very serious and reporting on flaky tests is an important step toward improvement +in that area. + +## Etiquette for pull requests + +### As an author + +Submitting pull requests should be done while adhering to a couple of simple rules. + +- Familiarize yourself with [coding style](STYLEGUIDE.md), architectural patterns and other contribution guidelines. +- No surprise PRs please. Before you submit a PR, open a discussion or an issue outlining your planned work and give + people time to comment. It may even be advisable to contact committers using the `@mention` feature. Unsolicited PRs + may get ignored or rejected. +- Create focused PRs: your work should be focused on one particular feature or bug. Do not create broad-scoped PRs that + solve multiple issues as reviewers may reject those PR bombs outright. +- Provide a clear description and motivation in the PR description in GitHub. This makes the reviewer's life much + easier. It is also helpful to outline the broad changes that were made, e.g. "Changes the schema of XYZ-Entity: + the `age` field changed from `long` to `String`". +- If you introduce new 3rd party dependencies, be sure to note them in the PR description and explain why they are + necessary. +- Stick to the established code style, please refer to the [styleguide document](STYLEGUIDE.md). +- All tests should be green, especially when your PR is in `"Ready for review"` +- Mark PRs as `"Ready for review"` only when you're prepared to defend your work. By that time you have completed your + work and shouldn't need to push any more commits other than to incorporate review comments. +- Merge conflicts should be resolved by squashing all commits on the PR branch, rebasing onto `main` and + force-pushing. Do this when your PR is ready to review. +- If you require a reviewer's input while it's still in draft, please contact the designated reviewer using + the `@mention` feature and let them know what you'd like them to look at. +- Re-request reviews after all remarks have been adopted. This helps reviewers track their work in GitHub. +- If you disagree with a committer's remarks, feel free to object and argue, but if no agreement is reached, you'll have + to either accept the decision or withdraw your PR. +- Be civil and objective. No foul language, insulting or otherwise abusive language will be tolerated. +- The PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). + - The title must follow the format as `(): `. + `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test` are allowed for + the ``. + - The length must be kept under 80 characters. + +### As a reviewer + +- Have a look at [Pull Request Review Pyramide](https://www.morling.dev/blog/the-code-review-pyramid/) +- Please complete reviews within two business days or delegate to another committer, removing yourself as a reviewer. +- If you have been requested as reviewer, but cannot do the review for any reason (time, lack of knowledge in particular + area, etc.) please comment that in the PR and remove yourself as a reviewer, suggesting a stand-in. +- Don't be overly pedantic. +- Don't argue basic principles (code style, architectural decisions, etc.) +- Use the `suggestion` feature of GitHub for small/simple changes. +- The following could serve you as a review checklist: + - no unnecessary dependencies in `build.gradle.kts` + - sensible unit tests, prefer unit tests over integration tests wherever possible (test runtime). Also check the + usage of test tags. + - code style + - simplicity and "uncluttered-ness" of the code + - overall focus of the PR +- Don't just wave through any PR. Please take the time to look at them carefully. +- Be civil and objective. No foul language, insulting or otherwise abusive language will be tolerated. The goal is to + _encourage_ contributions. + +## Contact Us + +If you have questions or suggestions, do not hesitate to contact the project developers via https://github.com/sovity. + +## Attribution + +This file is adapted from the [eclipse-edc](https://github.com/eclipse-dataspaceconnector/DataSpaceConnector) project. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..e4dafcf43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 sovity.de + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 000000000..ba070d181 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ + + + + + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![Apache 2.0][license-shield]][license-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + + +
+

+ +Logo + + +

sovity Community Edition EDC

+

+Extended EDC Connector by sovity. +
+Report Bug +· +Request Feature +

+
+ + +
+ Table of Contents +
    +
  1. About The Project
  2. +
  3. sovity Community Edition EDC
  4. +
  5. sovity Community Edition EDC Extensions
  6. +
  7. Compatibility
  8. +
  9. Getting Started
  10. +
  11. Contributing
  12. +
  13. License
  14. +
  15. Contact
  16. +
+
+ + + +## About The Project + +[Eclipse Dataspace Components](https://github.com/eclipse-edc) (EDC) is a framework +for building dataspaces, exchanging data securely with ensured data sovereignty. + +[sovity](https://sovity.de/) extends the EDC Connector's functionality with extensions to offer +enterprise-ready managed services like "Connector-as-a-Service", out-of-the-box fully configured DAPS +and integrations to existing other dataspace technologies. + +This repository contains our sovity Community Edition EDCs, containing pre-configured Open Source EDC Extensions. + +Check out our [Getting Started Section](#getting-started) on how to run a local sovity Community Edition EDC. + +

(back to top)

+ + + +## sovity Community Edition EDC + +Our sovity Community Edition EDC takes available Open Source EDC Extensions and combines them with our own +open source EDC Extensions from this repository to build ready-to-use EDC Docker Images. + +See [here](launchers/README.md) for a list of our sovity Community Edition EDC Docker Images. + +

(back to top)

+ +## sovity Community Edition EDC Extensions + +Feel free to explore and use our [EDC Extensions](./extensions) with your EDC setup. + +We packaged critical extensions for compatibility with our EDC UI and general usability features into +[sovity EDC Extensions Package](./extensions/sovity-edc-extensions-package). + +

(back to top)

+ +## Compatibility + +Our sovity Community Edition EDC and sovity Community Edition EDC Extensions are targeted to run with +our [sovity/edc-ui](https://github.com/sovity/edc-ui). + +Our sovity Community Edition EDC will use the current EDC Milestone with a certain delay +to ensure reliability with a new release. Earlier releases currently are not supported, but will be +supported, once the base EDC has a reliable version. + +

(back to top)

+ + + +## Getting Started + +The fastest way to get started is our [Getting Started Guide](docs/getting-started/README.md) +which takes you through the steps of either starting a local [docker-compose.yaml](docker-compose.yaml) or deploying a +productive sovity EDC CE or MDS EDC CE Connector. + +

(back to top)

+ + + + +## Contributing + +Contributions are what make the open source community such an amazing place to +learn, inspire, and create. Any contributions you make are **greatly +appreciated**. + +If you have a suggestion that would improve this project, please fork the repo and +create a pull request. You can also simply open +a [feature request](https://github.com/sovity/edc-extensions/issues/new?template=feature_request.md). Don't forget to +leave the project a ⭐, if you like the effort put into this version! + +Our contribution guideline can be found in [CONTRIBUTING.md](CONTRIBUTING.md). + +

(back to top)

+ + + +## License + +Distributed under the Apache 2.0 License. See `LICENSE` for more information. + +

(back to top)

+ + + +## Contact + +contact@sovity.de + +

(back to top)

+ + + + +[contributors-shield]: +https://img.shields.io/github/contributors/sovity/edc-extensions.svg?style=for-the-badge + +[contributors-url]: https://github.com/sovity/edc-extensions/graphs/contributors + +[forks-shield]: +https://img.shields.io/github/forks/sovity/edc-extensions.svg?style=for-the-badge + +[forks-url]: https://github.com/sovity/edc-extensions/network/members + +[stars-shield]: +https://img.shields.io/github/stars/sovity/edc-extensions.svg?style=for-the-badge + +[stars-url]: https://github.com/sovity/edc-extensions/stargazers + +[issues-shield]: +https://img.shields.io/github/issues/sovity/edc-extensions.svg?style=for-the-badge + +[issues-url]: https://github.com/sovity/edc-extensions/issues + +[license-shield]: +https://img.shields.io/github/license/sovity/edc-extensions.svg?style=for-the-badge + +[license-url]: https://github.com/sovity/edc-extensions/blob/main/LICENSE + +[linkedin-shield]: +https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 + +[linkedin-url]: https://www.linkedin.com/company/sovity diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..1b1c0bcd5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ +## Security + +sovity GmbH takes the security of its software products and services seriously, which includes all source code repositories managed through our GitHub organization: [sovity](https://github.com/sovity). + +If you believe you have found a security vulnerability in any of sovity's owned repositories, please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them via mail: [security@sovity.de](mailto:security@sovity.de) + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages + +We prefer all communications to be in English. + +## Attribution +This file is adapted from [eclipse-edc](https://github.com/eclipse-edc/DataDashboard) project. \ No newline at end of file diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md new file mode 100644 index 000000000..0de41a153 --- /dev/null +++ b/STYLEGUIDE.md @@ -0,0 +1,58 @@ +# Code Style Guide + +In order to maintain a coherent code style throughout the project we ask every contributor to adhere to a few simple +style guidelines. We assume most developers will use at least something like `IntelliJ` and therefore have support for +automatic code formatting, we are not going to list the guidelines here. If you absolutely want to take a look, checkout +the [config written in XML](docs/dev/checkstyle/checkstyle-config.xml). + +## Checkstyle configuration + +Checkstyle is a [tool](https://checkstyle.sourceforge.io/) that can statically analyze your source code to check against +a set of given rules. Those rules are formulated in an [XML document](docs/dev/checkstyle/checkstyle-config.xml). Many modern +IDEs have a plugin available for download that runs in the background and does code analysis. + +This checkstyle config is based off of the [Google Style](https://checkstyle.sourceforge.io/google_style.html) with a +few +additional rules such as the naming of constants and Types. + +_Note: currently we do **not** enforce the generation of Javadoc comments, even though documenting code is **highly** +recommended. We might enable this in the future, such that at least interfaces and public methods are commented._ + +## Running Checkstyle + +In order to get better usability and on-the-fly reporting, Checkstyle is available as IDE plugins for many modern IDEs, +and it can run either on-demand or continuously in the background: + +- [IntelliJ IDEA plugin [recommended]](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) +- [Eclipse IDE [recommended]](https://checkstyle.org/eclipse-cs/#!/) + +### Checkstyle as PR validation + +Apart from running Checkstyle locally as IDE plugin, we do run it on +our [Github Actions pipeline](.github/workflows/code_analysis.yml). At this time, Checkstyle will only spew out warnings, but +we may tighten the rules at a future time and without notice. This will result in failing Github Action pipelines. Also, +committers might reject PRs due to Checkstyle warnings. + +It is therefore **highly** recommended running Checkstyle locally as well. + +If you **do not wish** to run Checkstyle on you local machine, that's fine, but be prepared to get your PRs rejected +simply because of a stupid naming or formatting error. + +> _Note: we do not use the Checkstyle Gradle Plugin on Github Actions because violations would cause builds to fail. For +now, we only want to log warnings._ + +## [Recommended] IntelliJ Code Style Configuration + +If you are using Jetbrains IntelliJ IDEA, we have created a specific code style configuration that will automatically +format your source code according to that style guide. This should eliminate most of the potential Checkstyle violations +right from the get-go. You will need to reformat your code manually or in a pre-commit hook though. + +## [Optional] Generic `.editorconfig` + +For most other editors and IDEs we've supplied an [.editorconfig](.editorconfig) file that can be +placed at the appropriate location. The specific location will largely depend on your editor and your OS, please refer +to the [official documentation](https://editorconfig.org) for details. + +## Attribution + +This file is adapted from the [eclipse-edc](https://github.com/eclipse-dataspaceconnector/DataSpaceConnector) project. diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 000000000..ea8baafc3 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,48 @@ +# Summary + +* [Start](./README.md) +* [Connector Versions](./launchers/README.md) +* [Changelog](./CHANGELOG.md) + + +## User Documentation + +* [Getting Started](./docs/getting-started/README.md) +* [Data Transfer Modes](./docs/getting-started/documentation/data-transfer-methods.md) +* [API Wrapper](./docs/getting-started/documentation/api_wrapper.md) +* [OAuth Data Address](./docs/getting-started/documentation/oauth-data-address.md) +* [Parameterized Assets via UI](./docs/getting-started/documentation/parameterized_assets_via_ui.md) +* [Parameterized Assets via Managment API](./docs/getting-started/documentation/parameterized_assets.md) +* [Pull Data Transfer](./docs/getting-started/documentation/pull-data-transfer.md) + +## Deployment Documentation +* [Deployment Goal: Local Demo](./docs/deployment-guide/goals/local-demo) +* [Deployment Goal: Development](./docs/deployment-guide/goals/development) +* [Deployment Goal: Production](./docs/deployment-guide/goals/production) + * [Productive Deployment Guide](./docs/deployment-guide/goals/production) + * [Productive Deployment Guide 4.2.0 / MS8 / MDS 1.2](docs/deployment-guide/goals/production/4.2.0/README.md) + +## Developer Documentation + +* [Code of Conduct](./CODE_OF_CONDUCT.md) +* [Contribution Guide](./CONTRIBUTING.md) +* [Code-Style Guide](./STYLEGUIDE.md) +* [Security Guide](./SECURITY.md) + + +## Extensions + +* [API Wrapper](./extensions/wrapper/README.md) + * [Community Edition API](./extensions/wrapper/wrapper-api/README.md) + * [Enterprise Edition API](./extensions/wrapper/wrapper-ee-api/README.md) + * [Java API Client Library](./extensions/wrapper/clients/java-client/README.md) + * [Java API Client Library Example](./extensions/wrapper/clients/java-client-example/README.md) + * [TypeScript API Client Library](./extensions/wrapper/clients/typescript-client/README.md) + * [TypeScript API Client Library Example](./extensions/wrapper/clients/typescript-client-example/README.md) +* Policies + * [Always True](./extensions/policy-always-true/README.md) + * [Referring Connector](./extensions/policy-referring-connector/README.md) + * [Time Interval](./extensions/policy-time-interval/README.md) +* [Database Migration](./extensions/postgres-flyway/README.md) +* [EDC UI Config](./extensions/edc-ui-config/README.md) +* [Last Commit Info](./extensions/last-commit-info/README.md) diff --git a/UPDATES.md b/UPDATES.md new file mode 100644 index 000000000..62e413204 --- /dev/null +++ b/UPDATES.md @@ -0,0 +1,32 @@ + +# Updates checklist + +This is a checklist about the workarounds that we had to use and may cause trouble in the future. +These are not easily testable and require a manual check. + +--- + +After each EDC version update + +- [ ] Check if `org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder` added a new + field and adjust the builder in `de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder.fromEditMetadataRequest` accordingly + +## Context + +### Asset.toBuilder + +A list of the element that may break when updating the EDC version. + +In `de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder.fromEditMetadataRequest` + +When re-creating the asset, we can't re-use the `Asset.toBuilder()` as it doesn't allow us to remove properties. + +We must therefore re-build the asset using the same content as that `.toBuilder()`. + +If the Eclipse EDC adds a field in this builder, we will miss it and any write to the JsonLd via the web API +will remove that hypothetical new field. + +#### Workaround + +On the EDC version update, check that `org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder` doesn't set more +fields than what we set. If a new field was added, add it to this function too. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..678ef4a0d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,125 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.internal.impldep.org.jsoup.safety.Safelist.basic + +plugins { + id("java") + id("checkstyle") + id("maven-publish") +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val downloadArtifact: Configuration by configurations.creating { + isTransitive = false +} + + +val identityHubVersion: String by project +val registrationServiceVersion: String by project + +// task that downloads the RegSrv CLI and IH CLI +val getJars by tasks.registering(Copy::class) { + outputs.upToDateWhen { false } //always download + + from(downloadArtifact) + // strip away the version string + .rename { s -> + s.replace("-${identityHubVersion}", "") + .replace("-${registrationServiceVersion}", "") + .replace("-all", "") + } + into(layout.projectDirectory.dir("libs/cli-tools")) +} + +// run the download jars task after the "jar" task +tasks { + jar { + finalizedBy(getJars) + } +} + +allprojects { + apply(plugin = "java") + apply(plugin = "checkstyle") + + tasks.withType { + options.encoding = "UTF-8" + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() + } + + tasks.getByName("test") { + val runningOnGithub = System.getenv("GITHUB_CI")?.isNotBlank() ?: false + + useJUnitPlatform { + if (runningOnGithub) { + excludeTags = setOf("not-on-github") + } + } + + testLogging { + events = setOf(TestLogEvent.SKIPPED, TestLogEvent.FAILED) + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + } + failFast = true + } + + checkstyle { + toolVersion = "10.9.3" + configFile = rootProject.file("docs/dev/checkstyle/checkstyle-config.xml") + configDirectory.set(rootProject.file("docs/dev/checkstyle")) + maxErrors = 0 // does not tolerate errors + } + + repositories { + mavenCentral() + mavenLocal() + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + } + maven { + url = uri("https://maven.pkg.github.com/truzzt/mds-ap3") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + maven { + url = uri("https://maven.pkg.github.com/ids-basecamp/ids-infomodel-java") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + maven { + url = uri("https://pkgs.dev.azure.com/sovity/41799556-91c8-4df6-8ddb-4471d6f15953/_packaging/core-edc/maven/v1") + name = "AzureRepo" + } + } +} + +subprojects { + apply(plugin = "maven-publish") + + val sovityEdcExtensionsVersion: String by project + version = sovityEdcExtensionsVersion + + publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/sovity/edc-extensions") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + } + } +} diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml new file mode 100644 index 000000000..ac61499e8 --- /dev/null +++ b/docker-compose-dev.yaml @@ -0,0 +1,130 @@ +version: "3.8" +services: + edc-ui: + image: ${EDC_UI_IMAGE} + ports: + - '11000:8080' + - '33005:5005' + environment: + EDC_UI_ACTIVE_PROFILE: ${EDC_UI_ACTIVE_PROFILE} + EDC_UI_CONFIG_URL: edc-ui-config + EDC_UI_MANAGEMENT_API_URL: http://localhost:11002/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + EDC_UI_CATALOG_URLS: http://edc2:11003/api/dsp + EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD: http://localhost:11002/control/api/management + NGINX_ACCESS_LOG: off + + edc: + image: ${EDC_IMAGE} + depends_on: + - postgresql + environment: + MY_EDC_PARTICIPANT_ID: "my-edc" + MY_EDC_TITLE: "EDC Connector" + MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" + MY_EDC_CURATOR_URL: "https://example.com" + MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_MAINTAINER_URL: "https://sovity.de" + MY_EDC_MAINTAINER_NAME: "sovity GmbH" + + MY_EDC_FQDN: "edc" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://edc:11003/api/dsp + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: + - '11001:11001' + - '11002:11002' + - '11003:11003' + - '11004:11004' + - '11005:5005' + + edc-ui2: + image: ${EDC_UI_IMAGE} + ports: + - '22000:8080' + environment: + EDC_UI_ACTIVE_PROFILE: ${EDC_UI_ACTIVE_PROFILE} + EDC_UI_CONFIG_URL: edc-ui-config + EDC_UI_MANAGEMENT_API_URL: http://localhost:22002/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + EDC_UI_CATALOG_URLS: http://edc:11003/api/dsp + NGINX_ACCESS_LOG: off + + edc2: + image: ${EDC_IMAGE} + depends_on: + - postgresql2 + environment: + MY_EDC_PARTICIPANT_ID: "my-edc2" + MY_EDC_TITLE: "EDC Connector 2" + MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" + MY_EDC_CURATOR_URL: "https://example.com" + MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_MAINTAINER_URL: "https://sovity.de" + MY_EDC_MAINTAINER_NAME: "sovity GmbH" + + MY_EDC_FQDN: "edc2" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_JDBC_URL: jdbc:postgresql://postgresql2:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://edc2:11003/api/dsp + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: + - '22001:11001' + - '22002:11002' + - '22003:11003' + - '22004:11004' + - '22005:5005' + + postgresql: + image: docker.io/bitnami/postgresql:11 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + ports: + - '54321:5432' + volumes: + - 'postgresql:/bitnami/postgresql' + + postgresql2: + image: docker.io/bitnami/postgresql:11 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + ports: + - '54322:5432' + volumes: + - 'postgresql2:/bitnami/postgresql' + + test-backend: + image: ${TEST_BACKEND_IMAGE} + ports: + - '33001:11001' + +volumes: + postgresql: + driver: local + postgresql2: + driver: local diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..ac61499e8 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,130 @@ +version: "3.8" +services: + edc-ui: + image: ${EDC_UI_IMAGE} + ports: + - '11000:8080' + - '33005:5005' + environment: + EDC_UI_ACTIVE_PROFILE: ${EDC_UI_ACTIVE_PROFILE} + EDC_UI_CONFIG_URL: edc-ui-config + EDC_UI_MANAGEMENT_API_URL: http://localhost:11002/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + EDC_UI_CATALOG_URLS: http://edc2:11003/api/dsp + EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD: http://localhost:11002/control/api/management + NGINX_ACCESS_LOG: off + + edc: + image: ${EDC_IMAGE} + depends_on: + - postgresql + environment: + MY_EDC_PARTICIPANT_ID: "my-edc" + MY_EDC_TITLE: "EDC Connector" + MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" + MY_EDC_CURATOR_URL: "https://example.com" + MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_MAINTAINER_URL: "https://sovity.de" + MY_EDC_MAINTAINER_NAME: "sovity GmbH" + + MY_EDC_FQDN: "edc" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://edc:11003/api/dsp + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: + - '11001:11001' + - '11002:11002' + - '11003:11003' + - '11004:11004' + - '11005:5005' + + edc-ui2: + image: ${EDC_UI_IMAGE} + ports: + - '22000:8080' + environment: + EDC_UI_ACTIVE_PROFILE: ${EDC_UI_ACTIVE_PROFILE} + EDC_UI_CONFIG_URL: edc-ui-config + EDC_UI_MANAGEMENT_API_URL: http://localhost:22002/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + EDC_UI_CATALOG_URLS: http://edc:11003/api/dsp + NGINX_ACCESS_LOG: off + + edc2: + image: ${EDC_IMAGE} + depends_on: + - postgresql2 + environment: + MY_EDC_PARTICIPANT_ID: "my-edc2" + MY_EDC_TITLE: "EDC Connector 2" + MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" + MY_EDC_CURATOR_URL: "https://example.com" + MY_EDC_CURATOR_NAME: "Example GmbH" + MY_EDC_MAINTAINER_URL: "https://sovity.de" + MY_EDC_MAINTAINER_NAME: "sovity GmbH" + + MY_EDC_FQDN: "edc2" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_JDBC_URL: jdbc:postgresql://postgresql2:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://edc2:11003/api/dsp + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: + - '22001:11001' + - '22002:11002' + - '22003:11003' + - '22004:11004' + - '22005:5005' + + postgresql: + image: docker.io/bitnami/postgresql:11 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + ports: + - '54321:5432' + volumes: + - 'postgresql:/bitnami/postgresql' + + postgresql2: + image: docker.io/bitnami/postgresql:11 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc + ports: + - '54322:5432' + volumes: + - 'postgresql2:/bitnami/postgresql' + + test-backend: + image: ${TEST_BACKEND_IMAGE} + ports: + - '33001:11001' + +volumes: + postgresql: + driver: local + postgresql2: + driver: local diff --git a/docs/deployment-guide/goals/development/README.md b/docs/deployment-guide/goals/development/README.md new file mode 100644 index 000000000..b8af69d87 --- /dev/null +++ b/docs/deployment-guide/goals/development/README.md @@ -0,0 +1,69 @@ +Deployment Goal: Development +======== + +There is currently no way to launch running EDCs directly from our gradle projects. + +## Launching the `docker-compose-dev.yaml` + +To try out the latest snapshots of the EDC CE and EDC UI please run: + +## Dev Docker Compose + +To try out the latest **unstable** connector images with +the [docker-compose-dev.yaml](../../../../docker-compose-dev.yaml), execute: + + + + + + + + + + + + + + +
Launch two sovity EDC CE ConnectorsLaunch two MDS EDC CE
+ +```shell script +# Run with Bash from the root directory of the project + +# Log-In to the Github Container Registry +docker login ghcr.io + +# Pull the latest images +docker compose --env-file .env.dev -f docker-compose-dev.yaml pull + +# Start sovity EDC Connectors +docker compose --env-file .env.dev -f docker-compose-dev.yaml up +``` + + + +```shell script +# Run with Bash from the root directory of the project + +# Log-In to the Github Container Registry +docker login ghcr.io + +# Pull the latest images +docker compose --env-file .env.dev -f docker-compose-dev.yaml pull + +# Start MDS EDC Connectors +EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose --env-file .env.dev -f docker-compose-dev.yaml up +``` + +
+ +## Dev Docker Compose: Default Configuration + +The default configuration launches two local EDC Connectors with the following credentials: + +| | First Connector | Second Connector | +|---------------------|---------------------------------------------------------------|:---------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | +| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://edc:11003/api/dsp
Requires Docker Compose Network | http://edc2:22003/api/dsp
Requires Docker Compose Network | diff --git a/docs/deployment-guide/goals/local-demo/4.2.0/README.md b/docs/deployment-guide/goals/local-demo/4.2.0/README.md new file mode 100644 index 000000000..7120bb631 --- /dev/null +++ b/docs/deployment-guide/goals/local-demo/4.2.0/README.md @@ -0,0 +1,63 @@ +Deployment Goal: Local Demo +======== +> This is for an old major version sovity EDC CE 4.2.0. [Go back](../README.md) + +> On how to deploy a productive connector with joining an existing Data Space, please refer +> to our [Productive Deployment Guide](../../production/4.2.0/README.md). + +## Quick Start + +To quickly start using our sovity EDC CE or MDS EDC CE, we offer a quick +start [docker-compose.yaml](https://github.com/sovity/edc-extensions/blob/v4.2.0/docker-compose.yaml) file. + + + + + + + + + + + + + + +
Launch two sovity EDC CE ConnectorsLaunch two MDS EDC CE
+ +```shell script +# Run with Bash from the root directory of the project +# Use the release tag 4.2.0: https://github.com/sovity/edc-extensions/releases/tag/v4.2.0 + +# Log-In to the Github Container Registry +docker login ghcr.io + +# Start sovity EDC Connectors +docker compose up +``` + + + +```shell script +# Run with Bash from the root directory of the project +# Use the release tag 4.2.0: https://github.com/sovity/edc-extensions/releases/tag/v4.2.0 + +# Log-In to the Github Container Registry +docker login ghcr.io + +# Start MDS EDC Connectors +EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up +``` + +
+ +## Quick Start: Default Configuration + +The default configuration launches two local EDC Connectors with the following credentials: + +| | First Connector | Second Connector | +|---------------------|-----------------------------------------------------------------------|:--------------------------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | +| Management Endpoint | http://localhost:11002/api/v1/management | http://localhost:22002/api/v1/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://edc:11003/api/v1/ids/data
Requires Docker Compose Network | http://edc2:22003/api/v1/ids/data
Requires Docker Compose Network | diff --git a/docs/deployment-guide/goals/local-demo/README.md b/docs/deployment-guide/goals/local-demo/README.md new file mode 100644 index 000000000..4bd29ed37 --- /dev/null +++ b/docs/deployment-guide/goals/local-demo/README.md @@ -0,0 +1,62 @@ +Deployment Goal: Local Demo +======== + +> This is for our latest version. There is another guide for [4.2.0](4.2.0/README.md). + +> On how to deploy a productive connector with joining an existing Data Space, please refer +> to our [Productive Deployment Guide](../production/README.md). + +## Quick Start + +To quickly start using our sovity EDC CE or MDS EDC CE, we offer a quick +start [docker-compose.yaml](../../../../docker-compose.yaml) file. + + + + + + + + + + + + + + +
Launch two sovity EDC CE ConnectorsLaunch two MDS EDC CE
+ +```shell script +# Run with Bash from the root directory of the project + +# Log-In to the Github Container Registry +docker login ghcr.io + +# Start sovity EDC Connectors +docker compose up +``` + + + +```shell script +# Run with Bash from the root directory of the project + +# Log-In to the Github Container Registry +docker login ghcr.io + +# Start MDS EDC Connectors +EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up +``` + +
+ +## Quick Start: Default Configuration + +The default configuration launches two local EDC Connectors with the following credentials: + +| | First Connector | Second Connector | +|---------------------|---------------------------------------------------------------|:------------------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | +| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://edc:11003/api/dsp
Requires Docker Compose Network | http://edc2:22003/api/dsp
Requires Docker Compose Network | diff --git a/docs/deployment-guide/goals/production/4.2.0/README.md b/docs/deployment-guide/goals/production/4.2.0/README.md new file mode 100644 index 000000000..5ca9dcb49 --- /dev/null +++ b/docs/deployment-guide/goals/production/4.2.0/README.md @@ -0,0 +1,183 @@ +Deployment Goal: Production +======== + +> This is for an old major version sovity EDC CE 4.2.0. [Go back](../README.md) + +## About this Guide + +This is a productive deployment guide for self-hosting a functional sovity CE EDC Connector or MDS CE EDC Connector. + +## Requirements + +A productive EDC Connector deployment has strict requirements, with slight errors in configuration already causing +contract negotiations / data transfer to fail. + +In general a productive EDC Connector requires a DAPS Server, DAPS Credentials, a reverse proxy configured in detail due +to technical reasons, reachability via the internet and well-defined URLs across all configurations. + +## Deployment Units + +To deploy an EDC multiple deployment units must be deployed and configured. + +| Deployment Unit | Version / Details | +|----------------------------------------------------------------|------------------------------------------------------------------------------------------------| +| An Auth Proxy / Auth solution of your choice. | (deployment specific, required to secure UI and management API) | +| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | +| Postgresql | 13 or compatible version | +| EDC Backend | edc-ce or edc-ce-mds, see [CHANGELOG.md](../../../../../CHANGELOG.md) for compatible versions. | +| EDC UI | edc-ui, see [CHANGELOG.md](../../../../../CHANGELOG.md) for compatible versions. | + +## Configuration + +### Reverse Proxy Configuration + +To make the deployment work, the connector needs to be exposed to the internet. Connector-to-Connector +communication is asynchronous and done with authentication via the DAPS. Thus, if the target connector cannot reach +your connector under its self-declared URLs, contract negotiation and transfer processes will fail. + +The EDC Backend opens up multiple ports with different functionalities. They are expected to be merged by a reverse +proxy (at least the protocol endpoint needs to be). + +- The sovity EDC Connector is meant to be deployed with a reverse proxy merging the following ports: + - The UI's `80` port. Henceforth, called the UI. + - The Backend's `11002` port. Henceforth, called the Management API. + - The Backend's `11003` port. Henceforth, called the Protocol API. +- The mapping should look like this: + - `/api/v1/ids` -> `edc:11003/api/v1/ids` + - `/api/v1/management` -> `edc:11002/api/v1/management` + - All other requests should be mapped to `edc-ui:80` +- Regarding TLS/HTTPS: + - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. + - The UI and the Management API should have HTTP to HTTPS redirects. + - The Protocol API must allow HTTP traffic to pass through. This is due to some loopback requests + mistakenly using HTTP instead of HTTPS that would otherwise be blocked or have their credentials wiped. +- Regarding Authentication: + - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full + control of the application. + - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space + Authority / DAPS and the configured certificates. +- Exposing to the internet: + - The Protocol API must be reachable via the internet. The required endpoints can be found in + this [public-endpoints.yaml](public-endpoints.yaml) + - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. +- Security: + - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). + - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. + +## EDC UI Configuration + +A sovity EDC UI deployment requires the following config properties: + +```yaml +# Active Profile +EDC_UI_ACTIVE_PROFILE: sovity-open-source (or mds-open-source) + +# Management API URL +EDC_UI_DATA_MANAGEMENT_API_URL: https://[EDC URL]/api/v1/management + +# Management API Key +EDC_UI_DATA_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" + +# Enable config fetching from the backend +EDC_UI_CONFIG_URL: "edc-ui-config" +``` + +## EDC Backend Configuration + +A sovity EDC CE or MDS EDC CE Backend deployment requires: + +- A running DAPS +- (MDS Only) A running Clearing House +- DAPS Access + and [a generated SKI/AKI pair and .jks file](#faq) +- The following configuration properties + +> [!WARNING] +> Please be careful with overriding any of the ENV Vars set in our [launchers/.env](../../../../../launchers/.env). Our defaults +> will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as trailing +> slashes. + +```yaml +# Connector Host Name +MY_EDC_FQDN: "my-edc-deployment1.example.com" + +# Connector Technical Name +MY_EDC_NAME_KEBAB_CASE: "example-connector" + +# Connector Localized Name / Title +MY_EDC_TITLE: "EDC Connector" + +# Connector Description Text +MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" + +# Connector Curator +# The company using the EDC Connector, configuring data offers, etc. +MY_EDC_CURATOR_URL: "https://example.com" +MY_EDC_CURATOR_NAME: "Example GmbH" + +# Database Connection +MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc +MY_EDC_JDBC_USER: edc +MY_EDC_JDBC_PASSWORD: edc + +# Management API Key +# high entropy recommended when configuring the value (length, complexity, e.g. [a-zA-Z0-9+special chars]{32+ chars}) +EDC_API_AUTH_KEY: ApiKeyDefaultValue + +# Connector Maintainer +# The company hosting the EDC Connector +MY_EDC_MAINTAINER_URL: "https://sovity.de" +MY_EDC_MAINTAINER_NAME: "sovity GmbH" + +# (MDS Only) Clearing House +EDC_CLEARINGHOUSE_LOG_URL: 'https://clearing.test.mobility-dataspace.eu/messages/log' + +# DAPS URL +EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' +EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' + +# DAPS Credentials +EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' +EDC_KEYSTORE: '_path to .jks file in container_' +EDC_KEYSTORE_PASSWORD: '_your keystore password_' +EDC_OAUTH_CERTIFICATE_ALIAS: 1 +EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 +``` + +## FAQ + +### What should the client ID entry look like? + +Example of a client-ID entry: + +`EDC_OAUTH_CLIENT_ID: 7X:7Y:...:B2:94:keyid:6A:2B:...:28:80` + +### How do you get the SKI and AKI of a p12 and how do you convert it to a jks? + +You can use a script (if you're on WSL or Linux) to generate the SKI, AKI and jks file. + +1. Make sure you're on Linux or on a bash console (e.g. WSL or Git Bash) and have openssl and keytool installed +2. Navigate in the console to the resources/docs directory +3. Run the [script](../generate_ski_aki.sh) `./generate_ski_aki.sh [filepath].p12 [password]` and + substitute `[filepath]` to the p12 certificate + filepath and `[password]` to the certificate password +4. The jks file will be generated in the same folder as your p12 file and the SKI/AKI combination is printed out in the + console. + Copy the SKI:AKI combination and use it to start the EDC (optionally also save it to your password manager). +5. The generated `.jks` file needs to be mounted into your productive running container. + +### Can I run a connector locally and consume data from an online connector? + +No, locally run connectors cannot exchange data with online connectors. A connector must have a proper URL + +configuration and be accesible from the data provider via REST calls. + +### (MDS Only) Can I disable the Broker- and/or ClearingHouse-Client-Extensions dynamically? + +Yes, if the two extensions are included, they can still be disabled via properties. +The default settings can be found in `docker-compose.yaml` and can be changed there. + +```yaml +# Extension Configuration +BROKER_CLIENT_EXTENSION_ENABLED: false # disabled by default +CLEARINGHOUSE_CLIENT_EXTENSION_ENABLED: true # enabled by default +``` diff --git a/docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml b/docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml new file mode 100644 index 000000000..421213278 --- /dev/null +++ b/docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.1 +info: + title: sovity EDC CE Public Endpoints + description: Required publicly exposed EDC Connector endpoints + version: 4.2.0 +servers: + - url: https://[MY_EDC_FQDN] +tags: + - name: Protocol API + description: Port 11003 on the Backend Container. +paths: + /api/v1/ids/data: + post: + tags: + - Protocol API + description: IDS Message Endpoint + requestBody: + content: + multipart/form-data: + schema: + type: object + responses: + '200': + description: OK or empty response if token validation failed + content: + multipart/form-data: + schema: + type: object + + diff --git a/docs/deployment-guide/goals/production/README.md b/docs/deployment-guide/goals/production/README.md new file mode 100644 index 000000000..ca68cd0cb --- /dev/null +++ b/docs/deployment-guide/goals/production/README.md @@ -0,0 +1,244 @@ +Productive Deployment Guide +======== + +> This is for our latest version. There is another guide for [4.2.0](4.2.0/README.md). + +## About this Guide + +This is a productive deployment guide for self-hosting a functional sovity CE EDC Connector or MDS CE EDC Connector. + +## Prerequisites + +### Technical Skills + +- Ability to deploy, run and expose containered applications to the internet. +- Ability to configure ingress routes or a reverse proxy of your choice to merge multiple services under a single + domain. +- Know-how on how to secure an otherwise unprotected application with an auth proxy or other solutions fitting + your situation. + +### Dataspace + +- Must have a running DAPS that follows the subset of OAuth2 as described in the DSP Specification. +- You must have a valid Connector Certificate in the form of [a generated SKI/AKI pair and .jks file](#faq). +- You must have a valid Participant ID / Connector ID, which is configured in the claim "referringConnector" in the + DAPS. + +## Deployment Units + +To deploy an EDC multiple deployment units must be deployed and configured. + +| Deployment Unit | Version / Details | +|-------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| An Auth Proxy / Auth solution of your choice. | (deployment specific, required to secure UI and management API) | +| Reverse Proxy that merges multiple services and removes the ports | (deployment specific) | +| Postgresql | 13 or compatible version | +| EDC Backend | edc-ce or edc-ce-mds, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | +| EDC UI | edc-ui, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | + +## Configuration + +### Reverse Proxy Configuration + +To make the deployment work, the connector needs to be exposed to the internet. Connector-to-Connector +communication is asynchronous and done with authentication via the DAPS. Thus, if the target connector cannot reach +your connector under its self-declared URLs, contract negotiation and transfer processes will fail. + +The EDC Backend opens up multiple ports with different functionalities. They are expected to be merged by a reverse +proxy (at least the protocol endpoint needs to be). + +- The sovity EDC Connector is meant to be deployed with a reverse proxy merging the following ports: + - The UI's `8080` port. Henceforth, called the UI. + - The Backend's `11002` port. Henceforth, called the Management API. + - The Backend's `11003` port. Henceforth, called the Protocol API. +- The mapping should look like this: + - `https://[MY_EDC_FQDN]/api/dsp` -> `edc:11003/api/dsp` + - `https://[MY_EDC_FQDN]/api/management` -> **Auth Proxy** -> `edc:11002/api/management` + - All other requests -> **Auth Proxy** -> `edc-ui:80` +- Regarding TLS/HTTPS: + - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. + - All endpoint should have HTTP to HTTPS redirects. +- Regarding Authentication: + - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full + control of the application. + - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space + Authority / DAPS and the configured certificates. +- Exposing to the internet: + - The Protocol API must be reachable via the internet. The required endpoints can be found in + this [public-endpoints.yaml](public-endpoints.yaml) + - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. +- Security: + - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). + - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. + +## EDC UI Configuration + +A sovity EDC UI deployment requires the following config properties: + +```yaml +# Active Profile +EDC_UI_ACTIVE_PROFILE: sovity-open-source (or mds-open-source) + +# Management API URL +EDC_UI_MANAGEMENT_API_URL: https://[EDC URL]/api/management + +# Management API Key +EDC_UI_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" + +# Enable config fetching from the backend +EDC_UI_CONFIG_URL: "edc-ui-config" +``` + +You can also optionally set the following config properties: +```yaml +# Override the management API URL shown to the user in the UI +EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD: https://[EDC_URL]/api/control/management +``` + +## EDC Backend Configuration + +A sovity EDC CE or MDS EDC CE Backend deployment requires the following environment variables: + +> [!WARNING] +> Please be careful with overriding any of the ENV Vars set in our [launchers/.env](../../../../launchers/.env). Our +> defaults +> will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as trailing +> slashes. + +```yaml +# Connector Host Name +MY_EDC_FQDN: "my-edc-deployment1.example.com" + +# Participant ID / Connector ID +# Must be configured as the value of the "referringConnector" claim in the DAPS for this connector +MY_EDC_PARTICIPANT_ID: "MDSL1234XX.C1234XX" + +# Connector Localized Name / Title +MY_EDC_TITLE: "EDC Connector" + +# Connector Description Text +MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" + +# Connector Curator +# The company using the EDC Connector, configuring data offers, etc. +MY_EDC_CURATOR_URL: "https://example.com" +MY_EDC_CURATOR_NAME: "Example GmbH" + +# Database Connection +MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc +MY_EDC_JDBC_USER: edc +MY_EDC_JDBC_PASSWORD: edc + +# Management API Key +# high entropy recommended when configuring the value (length, complexity, e.g. [a-zA-Z0-9+special chars]{32+ chars}) +EDC_API_AUTH_KEY: ApiKeyDefaultValue + +# Connector Maintainer +# The company hosting the EDC Connector +MY_EDC_MAINTAINER_URL: "https://sovity.de" +MY_EDC_MAINTAINER_NAME: "sovity GmbH" + +# DAPS URL +EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' +EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' + +# DAPS Credentials +EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' +EDC_KEYSTORE: '_path to .jks file in container_' +EDC_KEYSTORE_PASSWORD: '_your keystore password_' +EDC_OAUTH_CERTIFICATE_ALIAS: 1 +EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 +``` + +A LoggingHouse extension is included in the MDS variant, which means that additional properties must be set for it: +```yaml +# LoggingHouse Extension +EDC_LOGGINGHOUSE_EXTENSION_ENABLED: "true" +EDC_LOGGINGHOUSE_EXTENSION_URL: https://clearing.test.mobility-dataspace.eu +``` + +You can also optionally set the following config properties: +```yaml +# Enables DEBUG-Level logging +DEBUG_LOGGING: true + +# Enables JDWP Remote Debugging +REMOTE_DEBUG: true +REMOTE_DEBUG_SUSPEND: true # default: false +REMOTE_DEBUG_BIND: 127.0.0.1:5005 # default: 127.0.0.1:5005 +``` + +## FAQ + +### What should the client ID entry look like? + +Example of a client-ID entry: + +`EDC_OAUTH_CLIENT_ID: 7X:7Y:...:B2:94:keyid:6A:2B:...:28:80` + +### How do you get the SKI and AKI of a p12 and how do you convert it to a jks? + +You can use a script (if you're on WSL or Linux) to generate the SKI, AKI and jks file. + +1. Make sure you're on Linux or on a bash console (e.g. WSL or Git Bash) and have openssl and keytool installed +2. Navigate in the console to the resources/docs directory +3. Run the [script](./generate_ski_aki.sh) `./generate_ski_aki.sh [filepath].p12 [password]` and + substitute `[filepath]` to the p12 certificate + filepath and `[password]` to the certificate password +4. The jks file will be generated in the same folder as your p12 file and the SKI/AKI combination is printed out in the + console. + Copy the SKI:AKI combination and use it to start the EDC (optionally also save it to your password manager). +5. The generated `.jks` file needs to be mounted into your productive running container. + +### Can I run a connector locally and consume data from an online connector? + +No, locally run connectors cannot exchange data with online connectors. A connector must have a proper URL + +configuration and be accesible from the data provider via REST calls. + +### Can I use a different DAT Claim for the Participant ID verification? + +The checked DAT claim name can be changed by overriding `EDC_AGENT_IDENTITY_KEY`. However, this must be done in sync +with all connectors of the data space for contract negotations and transfers to work. + +### Can I change the Participant ID of my connector? + +You can always re-start your connector with a different Participant ID. Please make sure your changed Participant ID is +deposited in the DAPS as new Contract Negotiations or Transfer Processes will validate the Participant ID of each +connector. Both connectors must also be configured to check for the same claim. + +After changing your Participant ID old Contract Agreements will stop working, because the Participant ID is heavily +referenced in both connectors, and there is no way for the other connector to know what your Participant ID changed to. + +This is relevant, because for MS8 connectors the Participant ID concept did not exist yet or was not enforced in any +way, which might force participants to re-negotiate old contracts. + +### What if I have no Participant ID / Connector ID concept in my Dataspace? + +If there is no Participant ID / Connector ID concept in your Dataspace, you could use the AKI / SKI Client ID as +Participant ID / Connector ID: + +```yaml +# Using the SKI / AKI Client ID as Participant ID +MY_EDC_PARTICIPANT_ID: '_your SKI/AKI_' + +# Claim Name of the AKI / SKI Client ID: +EDC_AGENT_IDENTITY_KEY: 'sub' # or 'client_id' in Omejdn +``` + +The downside to doing this is that the AKI / SKI Client ID is not human-readable, but will be shown in many places. + +### Can I still use the deprecated Omejdn DAPS? + +In the current version of the sovity EDC CE Connector the Omejdn DAPS is not supported due to the Omejdn DAPS requiring +a special OAuth2 extension and custom messages that exceed the default DSP Oauth2 Specification. + +When using the required extension, these additional env variables would be required for the backend to be configured for +the Omejdn DAPS: + +```yaml +# Required Config for an Omejdn DAPS: +MY_EDC_PARTICIPANT_ID: '_your SKI/AKI_' +EDC_AGENT_IDENTITY_KEY: 'client_id' +EDC_OAUTH_PROVIDER_AUDIENCE: 'idsc:IDS_CONNECTORS_ALL' +EDC_OAUTH_ENDPOINT_AUDIENCE: 'idsc:IDS_CONNECTORS_ALL' +``` diff --git a/docs/deployment-guide/goals/production/generate_ski_aki.sh b/docs/deployment-guide/goals/production/generate_ski_aki.sh new file mode 100644 index 000000000..b99931d91 --- /dev/null +++ b/docs/deployment-guide/goals/production/generate_ski_aki.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +if [ ! $# -eq 2 ] + then + echo "Provide X509 keystore file as parameter along keystore's password, e.g. \"$ generate_ski_aki.sh ./mycert.p12 PASSWORD\"" + exit 1 +fi + +P12_FILE=$1 +PASSWORD=$2 +P12_ENDING=".p12" +JKS_ENDING=".jks" + +JKS_FILE=${P12_FILE//".p12"/".jks"} +TEMP_FILE="./temp.cert" +CRT_FILE="./cert.crt" + + + +if [ -n "$P12_FILE" ]; then + [ ! -f "$P12_FILE" ] && (echo "Cert not found"; exit 1) + if [[ $(openssl version) == *"1.1.1"* ]]; + then + openssl pkcs12 -in "$P12_FILE" -clcerts -nokeys -out "$CRT_FILE" -passin "pass:$PASSWORD" + else + openssl pkcs12 -in "$P12_FILE" -clcerts -nokeys -legacy -out "$CRT_FILE" -passin "pass:$PASSWORD" + fi + openssl x509 -in "$CRT_FILE" -text > "$TEMP_FILE" + keytool -importkeystore -srckeystore "$P12_FILE" -srcstoretype pkcs12 -destkeystore "$JKS_FILE" -deststoretype jks -deststorepass "$PASSWORD" -srcstorepass "$PASSWORD" -noprompt 2>/dev/null +fi + +SKI="$(grep -A1 "Subject Key Identifier" "$TEMP_FILE" | tail -n 1 | tr -d ' ')" +AKI="$(grep -A1 "Authority Key Identifier" "$TEMP_FILE" | tail -n 1 | tr -d ' ')" + +if [[ $AKI == "keyid"* ]]; then + echo "$SKI:$AKI" +else + echo "$SKI:keyid:$AKI" +fi + +rm "$TEMP_FILE" +rm "$CRT_FILE" \ No newline at end of file diff --git a/docs/deployment-guide/goals/production/public-endpoints.yaml b/docs/deployment-guide/goals/production/public-endpoints.yaml new file mode 100644 index 000000000..c3ae39932 --- /dev/null +++ b/docs/deployment-guide/goals/production/public-endpoints.yaml @@ -0,0 +1,650 @@ +openapi: 3.0.1 +info: + title: sovity EDC CE Public Endpoints + version: 5.0.0 + description: | + These are the required publicly exposed endpoints on our sovity EDC Community Edition Connectors. + + As the Eclipse EDC Connectors uses Data Space Protocol (DSP) for connector-to-connector communication, this includes all protocol endpoints. + + Please note that the DSP HTTP endpoints use JSON-LD documents, which must be semantically interpreted and thus cannot be represented correctly via OpenAPI. + + Links: + - [DSP Version 0.8 Full Specification](https://github.com/International-Data-Spaces-Association/ids-specification/tree/v0.8) + - [DSP Version 0.8 HTTP OpenAPI](https://github.com/boschresearch/py-cx-ids/blob/ed62e4ad92e4715551e081a9d27f58ea6919faaa/pycxids/core/http_binding/http_binding_openapi.yaml) by Matthias Binzer + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +servers: + - url: https://[MY_EDC_FQDN]/api/dsp +tags: + - name: "Catalog" + description: "Catalog endpoints" + + - name: "Negotiation" + description: "Negotiation Process - Provider side" + + - name: "Negotiation Callbacks" + description: "Negotiation Process Callbacks - Consumer side" + + - name: "Transfer" + description: "Transfer Process" + +paths: + /catalog/request: + post: + tags: + - "Catalog" + requestBody: + required: True + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogRequestMessage" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/DcatCatalog" + description: "Result" + /catalog/datasets/{id}: + get: + tags: + - "Catalog" + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Result" + content: + application/json: + schema: + $ref: "#/components/schemas/DcatDataset" + + /negotiations/{id}: + get: + tags: + - "Negotiation" + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Result" + content: + application/json: + schema: + $ref: "#/components/schemas/ContractNegotiation" + + /negotiations/request: + post: + tags: + - "Negotiation" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractRequestMessage" + responses: + "201": + description: "The provider connector must return an HTTP 201 (Created) response with the location header set to the location of the contract negotiation and a body containing the ContractNegotiation message" + content: + application/json: + schema: + $ref: "#/components/schemas/ContractNegotiation" + /negotiations/{id}/request: + post: + tags: + - "Negotiation" + description: "The consumer must include the processId. The consumer must include either the offer or offerId property." + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractRequestMessage" + responses: + "200": + description: "If the message is successfully processed, the provider connector must return and HTTP 200 (OK) response. The response body is not specified and clients are not required to process it." + /negotiations/{id}/events: + post: + tags: + - "Negotiation" + description: "The consumer must include the processId. The consumer must include either the offer or offerId property." + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractNegotiationEventMessage" + responses: + "200": + description: "Result" + /negotiations/{id}/agreement/verification: + post: + tags: + - "Negotiation" + description: "TODO" + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractAgreementVerificationMessage" + responses: + "200": + description: "Result" + /negotiations/{id}/termination: + post: + tags: + - "Negotiation" + description: "TODO" + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractNegotiationTerminationMessage" + responses: + "200": + description: "Result" + + /negotiations/{id}/offer: + post: + tags: + - "Negotiation Callbacks" + description: "Callback on the Consumer side" + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractOfferMessage" + responses: + "200": + description: "Result" + /negotiations/{id}/agreement: + post: + tags: + - "Negotiation Callbacks" + description: "Callback on the Consumer side" + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContractAgreementMessage" + responses: + "200": + description: "Result" + + /transfers/request: + post: + tags: + - "Transfer" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TransferRequestMessage" + responses: + "201": + description: "The provider connector must return an HTTP 201 (Created) response with the location header set to the location of the transfer process and a body containing the TransferProcess message" + content: + application/json: + schema: + $ref: "#/components/schemas/TransferProcess" + + +components: + schemas: + + DspaceFilter: + type: object + + CatalogRequestMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "TODO: not finished yet" + properties: + "@type": + type: string + default: "dspace:CatalogRequestMessage" + filter: + $ref: "#/components/schemas/DspaceFilter" + + ContractRequestMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "TODO: not finished yet" + properties: + "@type": + type: string + default: "dspace:ContractRequestMessage" + dspace:dataset: + type: string + description: "@id of the dataset" + dspace:dataSet: + type: string + description: "Only there for compatibility reasons. Seems to be a type in the spec" + dspace:processId: + type: string + description: "TODO: Deprecated? To be removed?" + dspace:offer: + $ref: "#/components/schemas/OdrlOffer" + dspace:callbackAddress: + type: string + + ContractNegotiationEventMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "TODO: not finished yet" + properties: + "@type": + type: string + default: "dspace:ContractNegotiationEventMessage" + "dspace:processId": + type: string + "dspace:eventType": + type: string + enum: + - "https://w3id.org/dspace/v0.8/FINALIZED" + - "https://w3id.org/dspace/v0.8/ACCEPTED" + - "https://w3id.org/dspace/v0.8/TERMINATED" + + ContractAgreementVerificationMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "TODO: not finished yet" + properties: + "@type": + type: string + default: "dspace:ContractAgreementVerificationMessage" + "dspace:processId": + type: string + + ContractNegotiationTerminationMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "TODO: not finished yet" + properties: + "@type": + type: string + default: "dspace:ContractNegotiationTerminationMessage" + "dspace:processId": + type: string + "dspace:code": + type: string + description: "TODO: not documented?" + "dspace:reason": + description: "TODO: can be a link / @id too" + type: array + items: + $ref: "#/components/schemas/LanguageValue" + + ContractOfferMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "Used for Tranistion Requested -> Offered from Provider -> Consumer Callback endpoint" + properties: + "@type": + type: string + default: "dspace:ContractOfferMessage" + dspace:processId: + type: string + dspace:offer: + $ref: "#/components/schemas/OdrlOffer" + dspace:callbackAddress: + type: string + + ContractAgreementMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "Used for Tranistion Requested -> Agreed from Provider -> Consumer Callback endpoint" + properties: + "@type": + type: string + default: "dspace:ContractAgreementMessage" + dspace:processId: + type: string + dspace:agreement: + $ref: "#/components/schemas/OdrlAgreement" + dspace:callbackAddress: + type: string + + + ContractNegotiation: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + properties: + "@type": + type: string + default: "dspace:ContractNegotiation" + "dspace:processId": + type: string + "dspace:state": + $ref: "#/components/schemas/TransferState" + + TransferRequestMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "Used for Transfer Tranistion Start -> Requested from Consumer -> Provider" + properties: + "@type": + type: string + default: "dspace:TransferRequestMessage" + dspace:agreementId: + type: string + description: "The agreementId property refers to an existing contract agreement between the consumer and provider." + dct:format: + type: string + description: "The dct:format property is a format specified by a Distribution for the Asset associated with the agreement. This is generally obtained from the provider Catalog." + dspace:dataAddress: + type: string + description: "The dataAddress property must only be provided if the dct:format requires a push transfer." + dspace:callbackAddress: + type: string + + TransferStartMessage: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + description: "Used for Transfer Tranistion Requested -> Started from Provider -> Consumer" + properties: + "@type": + type: string + default: "dspace:TransferStartMessage" + dspace:processId: + type: string + dspace:dataAddress: + oneOf: + - type: object + additionalProperties: + type: string + - type: string + + TransferProcess: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + properties: + "@type": + type: string + default: "dspace:TransferProcess" + dspace:processId: + type: string + dspace:state: + $ref: "#/components/schemas/TransferState" + + NegotiationState: + type: string + enum: + - REQUESTED + - OFFERED + - ACCEPTED + - AGREED + - VERIFIED + - FINALIZED + - TERMINATED + + TransferState: + type: string + enum: + - REQUESTED + - STARTED + - TERMINATED + - COMPLETED + - SUSPENDED + + + LanguageValue: + type: object + properties: + "@value": + type: string + "@language": + type: string + + JsonLd: + type: object + properties: + "@context": + type: object + items: + type: string + default: { + "dct": "https://purl.org/dc/terms/", + "tx": "https://w3id.org/tractusx/v0.0.1/ns/", + "edc": "https://w3id.org/edc/v0.0.1/ns/", + "dcat": "https://www.w3.org/ns/dcat/", + "odrl": "http://www.w3.org/ns/odrl/2/", + "dspace": "https://w3id.org/dspace/v0.8/" + } + "@id": + type: string + + + DcatDataset: + type: object + properties: + "@id": + type: string + "@type": + type: string + default: "dcat:Dataset" + dct:title: + type: string + dct:description: + type: string + dct:keyword: + type: array + items: + type: string + odrl:hasPolicy: + type: array + items: + $ref: "#/components/schemas/OdrlPolicy" + dcat:distribution: + $ref: "#/components/schemas/DcatDistribution" + + DcatCatalog: + allOf: + - $ref: "#/components/schemas/JsonLd" + - type: object + properties: + "@type": + type: string + default: "dcat:Catalog" + "dct:title": + type: string + "dct:publisher": + type: string + "dcat:keyword": + type: array + items: + type: string + dcat:service: + $ref: "#/components/schemas/DcatService" + dcat:dataset: + type: array + items: + $ref: "#/components/schemas/DcatDataset" + default: [] + + DcatDistribution: + type: object + properties: + "@type": + type: string + dct:format: + type: string + dcat:accessService: + type: string + + DcatService: + type: object + properties: + "@id": + type: string + "@type": + type: string + dct:terms: + type: string + dct:endpointUrl: + type: string + + + OdrlOperand: + type: object + properties: + value: + type: string + OdrlConstraint: + type: object + properties: + odrl:leftOperand: + $ref: "#/components/schemas/OdrlOperand" + odrl:rightOperand: + $ref: "#/components/schemas/OdrlOperand" + odrl:operator: + type: string + + Action: + type: object + properties: + odrl:type: + type: string + + OdrlRule: + type: object + properties: + odrl:action: + oneOf: + - type: string + - $ref: "#/components/schemas/Action" + odrl:constraint: + $ref: "#/components/schemas/OdrlConstraint" + odrl:duty: + type: array + items: + type: string # TODO: what is this exactly? + OdrlPolicy: + description: "In IDS http binding explicitly does NOT have a target, because this is derived from the enclosing context!" + type: object + properties: + "@id": + type: string + "@type": + type: string + default: "odrl:Policy" + odrl:permission: + oneOf: + - $ref: "#/components/schemas/OdrlRule" + - type: array + items: + $ref: "#/components/schemas/OdrlRule" + odrl:prohibition: + oneOf: + - $ref: "#/components/schemas/OdrlRule" + - type: array + items: + $ref: "#/components/schemas/OdrlRule" + odrl:obligation: + oneOf: + - $ref: "#/components/schemas/OdrlRule" + - type: array + items: + $ref: "#/components/schemas/OdrlRule" + OdrlOffer: + description: "Only addition compared to the Policy: the target field" + allOf: + - $ref: "#/components/schemas/OdrlPolicy" + - type: object + properties: + "@type": + type: string + default: "odrl:Offer" + odrl:target: + type: string + OdrlAgreement: + description: "Agreement" + allOf: + - $ref: "#/components/schemas/OdrlPolicy" + - type: object + properties: + "@type": + type: string + default: "odrl:Agreement" + odrl:target: + type: string + dspace:timestamp: + oneOf: + - $ref: "#/components/schemas/DspaceTimestamp" + - type: string + dspace:consumerId: + type: string + dspace:providerId: + type: string + DspaceTimestamp: + description: "xsd:dateTime" + properties: + "@type": + type: string + default: "xsd:dateTime" + "@value": + type: string \ No newline at end of file diff --git a/docs/dev/changelog_updates.md b/docs/dev/changelog_updates.md new file mode 100644 index 000000000..528935c54 --- /dev/null +++ b/docs/dev/changelog_updates.md @@ -0,0 +1,60 @@ +Updating the Changelog +====================== + +This project uses a [CHANGELOG.md](../../CHANGELOG.md). + +## Structure of the Changelog + +Each pull request should also update the "Unreleased" section of the changelog. +It should also update the "Deployment Migration Notes" Section of the unreleased section as preparation for the release. + +For each release there will be a separate section especially with an "Overview" section containing a summary +from a product perspective. + +Releases will especially contain a "Compatible Versions" section with the final docker +images and versions of our Connector Backend and Frontend. + +## How to categorize a change + +The changelog uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Changes are categorized as either Major, Minor or Patch Changes. + +For this project, changes are categorized as the following: + +### Major Changes + +Major changes include: + +- UX / Product overhauls. +- Breaking changes in Connector-To-Connector communication. +- Breaking changes to other deployment units (our UI doesn't count). +- Breaking changes in our API Wrapper Use Case API. + +### Minor Changes + +Minor changes include: + +- Any changes from a product perspective to our UI or API Wrapper UI API. +- Additions to our API Wrapper Use Case API. +- New Product Documentation + +### Patch Changes + +Patch changes are basically everything else: + +- Product Fixes, Bugfixes, Refactorings +- Changes to existing Product Documentation +- New or changes to Developer Documentation +- Everything else + +## Released Versions + +On releases the "Unreleased" section is emptied in favor of a new section for the release. + +Whether a release will bump the major, minor or patch version is decided by the unreleased changes in the changelog. + +The Release sections will be cleaned up on release, improved with additional information and made +useful for the customer and people deploying the application, containing both product changes and +deployment migration notes. + +More on that can be found in the [Release Issue Template](../../.github/ISSUE_TEMPLATE/release.md). diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml new file mode 100644 index 000000000..dc848298a --- /dev/null +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/eclipse-edc-management-api.yaml b/docs/eclipse-edc-management-api.yaml new file mode 100644 index 000000000..181094d05 --- /dev/null +++ b/docs/eclipse-edc-management-api.yaml @@ -0,0 +1,2818 @@ +openapi: 3.0.1 +info: + title: management-api + description: REST API documentation for the Eclipse EDC management-api. This does not include endpoints of the sovity EDC API Wrapper. + version: 0.2.1 +servers: + - url: https://[MY_EDC_FQDN]/api/management +paths: + /callback/{processId}/deprovision: + post: + tags: + - HTTP Provisioner Webhook + operationId: callDeprovisionWebhook + parameters: + - name: processId + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeprovisionedResource' + responses: + default: + description: default response + content: + application/json: { } + /callback/{processId}/provision: + post: + tags: + - HTTP Provisioner Webhook + operationId: callProvisionWebhook + parameters: + - name: processId + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionerWebhookRequest' + responses: + default: + description: default response + content: + application/json: { } + /check/health: + get: + tags: + - Application Observability + description: Performs a liveness probe to determine whether the runtime is working + properly. + operationId: checkHealth + responses: + "200": + description: The runtime is working properly. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + /check/liveness: + get: + tags: + - Application Observability + description: Performs a liveness probe to determine whether the runtime is working + properly. + operationId: getLiveness + responses: + "200": + description: The runtime is working properly. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + /check/readiness: + get: + tags: + - Application Observability + description: Performs a readiness probe to determine whether the runtime is + able to accept requests. + operationId: getReadiness + responses: + "200": + description: The runtime is able to accept requests. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + /check/startup: + get: + tags: + - Application Observability + description: Performs a startup probe to determine whether the runtime has completed + startup. + operationId: getStartup + responses: + "200": + description: The runtime has completed startup. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + /select: + post: + tags: + - Dataplane Selector + description: Finds the best fitting data plane instance for a particular query + operationId: find + requestBody: + content: + '*/*': + schema: + $ref: '#/components/schemas/SelectionRequestSchema' + responses: + "200": + description: The DataPlane instance that fits best for the given selection + request + content: + '*/*': + schema: + $ref: '#/components/schemas/DataPlaneInstanceSchema' + "204": + description: No suitable DataPlane instance was found + "400": + description: Request body was malformed + content: + '*/*': + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/assets: + put: + tags: + - Asset + description: "Updates an asset with the given ID if it exists. If the asset\ + \ is not found, no further action is taken. DANGER ZONE: Note that updating\ + \ assets can have unexpected results, especially for contract offers that\ + \ have been sent out or are ongoing in contract negotiations." + operationId: updateAsset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + responses: + "200": + description: Asset was updated successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: "Asset could not be updated, because it does not exist." + deprecated: true + post: + tags: + - Asset + description: Creates a new asset together with a data address + operationId: createAsset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AssetEntryNewDto' + responses: + "200": + description: Asset was created successfully. Returns the asset Id and created + timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "Could not create asset, because an asset with that ID already\ + \ exists" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/assets/request: + post: + tags: + - Asset + description: ' all assets according to a particular query' + operationId: requestAssets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The assets matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/Asset' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/assets/{assetId}/dataaddress: + put: + tags: + - Asset + description: Updates a DataAddress for an asset with the given ID. + operationId: updateDataAddress + parameters: + - name: assetId + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + responses: + "200": + description: Asset was updated successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/assets/{id}: + get: + tags: + - Asset + description: Gets an asset with the given ID + operationId: getAsset + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The asset + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + delete: + tags: + - Asset + description: "Removes an asset with the given ID if possible. Deleting an asset\ + \ is only possible if that asset is not yet referenced by a contract agreement,\ + \ in which case an error is returned. DANGER ZONE: Note that deleting assets\ + \ can have unexpected results, especially for contract offers that have been\ + \ sent out or ongoing or contract negotiations." + operationId: removeAsset + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Asset was deleted successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "The asset cannot be deleted, because it is referenced by a\ + \ contract agreement" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/assets/{id}/dataaddress: + get: + tags: + - Asset + description: Gets a data address of an asset with the given ID + operationId: getAssetDataAddress + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The data address + content: + application/json: + schema: + $ref: '#/components/schemas/DataAddress' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/catalog/dataset/request: + post: + tags: + - Catalog + operationId: getDataset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetRequest' + responses: + default: + description: Gets single dataset from a connector + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + /v2/catalog/request: + post: + tags: + - Catalog + operationId: requestCatalog + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogRequest' + responses: + default: + description: Gets contract offers (=catalog) of a single connector + content: + application/json: + schema: + $ref: '#/components/schemas/Catalog' + /v2/contractagreements/request: + post: + tags: + - Contract Agreement + description: Gets all contract agreements according to a particular query + operationId: queryAllAgreements + requestBody: + content: + '*/*': + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The contract agreements matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ContractAgreement' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractagreements/{id}: + get: + tags: + - Contract Agreement + description: Gets an contract agreement with the given ID + operationId: getAgreementById + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The contract agreement + content: + application/json: + schema: + $ref: '#/components/schemas/ContractAgreement' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An contract agreement with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractagreements/{id}/negotiation: + get: + tags: + - Contract Agreement + description: Gets a contract negotiation with the given contract agreement ID + operationId: getNegotiationByAgreementId + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The contract negotiation + content: + application/json: + schema: + $ref: '#/components/schemas/ContractNegotiation' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An contract agreement with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractdefinitions: + put: + tags: + - Contract Definition + description: Updated a contract definition with the given ID. The supplied JSON + structure must be a valid JSON-LD object + operationId: updateContractDefinition + requestBody: + content: + '*/*': + schema: + $ref: '#/components/schemas/ContractDefinitionInput' + responses: + "204": + description: Contract definition was updated successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract definition with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + post: + tags: + - Contract Definition + description: Creates a new contract definition + operationId: createContractDefinition + requestBody: + content: + '*/*': + schema: + $ref: '#/components/schemas/ContractDefinitionInput' + responses: + "200": + description: contract definition was created successfully. Returns the Contract + Definition Id and created timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "Could not create contract definition, because a contract definition\ + \ with that ID already exists" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractdefinitions/request: + post: + tags: + - Contract Definition + description: Returns all contract definitions according to a query + operationId: queryAllContractDefinitions + requestBody: + content: + '*/*': + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The contract definitions matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ContractDefinitionOutput' + "400": + description: Request was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractdefinitions/{id}: + get: + tags: + - Contract Definition + description: Gets an contract definition with the given ID + operationId: getContractDefinition + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The contract definition + content: + application/json: + schema: + $ref: '#/components/schemas/ContractDefinitionOutput' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An contract agreement with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + delete: + tags: + - Contract Definition + description: "Removes a contract definition with the given ID if possible. DANGER\ + \ ZONE: Note that deleting contract definitions can have unexpected results,\ + \ especially for contract offers that have been sent out or ongoing or contract\ + \ negotiations." + operationId: deleteContractDefinition + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Contract definition was deleted successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract definition with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractnegotiations: + post: + tags: + - Contract Negotiation + description: "Initiates a contract negotiation for a given offer and with the\ + \ given counter part. Please note that successfully invoking this endpoint\ + \ only means that the negotiation was initiated. Clients must poll the /{id}/state\ + \ endpoint to track the state" + operationId: initiateContractNegotiation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContractRequest' + responses: + "200": + description: The negotiation was successfully initiated. Returns the contract + negotiation ID and created timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + links: + poll-state: + operationId: getNegotiationState + parameters: + id: $response.body#/id + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractnegotiations/request: + post: + tags: + - Contract Negotiation + description: Returns all contract negotiations according to a query + operationId: queryNegotiations + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The contract negotiations that match the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ContractNegotiation' + "400": + description: Request was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractnegotiations/{id}: + get: + tags: + - Contract Negotiation + description: Gets a contract negotiation with the given ID + operationId: getNegotiation + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The contract negotiation + content: + application/json: + schema: + $ref: '#/components/schemas/ContractNegotiation' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractnegotiations/{id}/agreement: + get: + tags: + - Contract Negotiation + description: Gets a contract agreement for a contract negotiation with the given + ID + operationId: getAgreementForNegotiation + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: "The contract agreement that is attached to the negotiation,\ + \ or null" + content: + application/json: + schema: + $ref: '#/components/schemas/ContractAgreement' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractnegotiations/{id}/cancel: + post: + tags: + - Contract Negotiation + description: "Requests aborting the contract negotiation. Due to the asynchronous\ + \ nature of contract negotiations, a successful response only indicates that\ + \ the request was successfully received. Clients must poll the /{id}/state\ + \ endpoint to track the state." + operationId: cancelNegotiation + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Request to cancel the Contract negotiation was successfully + received + links: + poll-state: + operationId: getNegotiationState + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/contractnegotiations/{id}/decline: + post: + tags: + - Contract Negotiation + description: "Requests cancelling the contract negotiation. Due to the asynchronous\ + \ nature of contract negotiations, a successful response only indicates that\ + \ the request was successfully received. Clients must poll the /{id}/state\ + \ endpoint to track the state." + operationId: declineNegotiation + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Request to decline the Contract negotiation was successfully + received + links: + poll-state: + operationId: getNegotiationState + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true + /v2/contractnegotiations/{id}/state: + get: + tags: + - Contract Negotiation + description: Gets the state of a contract negotiation with the given ID + operationId: getNegotiationState + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The contract negotiation's state + content: + application/json: + schema: + $ref: '#/components/schemas/NegotiationState' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/contractnegotiations/{id}/terminate: + post: + tags: + - Contract Negotiation + description: Terminates the contract negotiation. + operationId: terminateNegotiation + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TerminateNegotiationSchema' + responses: + "200": + description: ContractNegotiation is terminating + links: + poll-state: + operationId: getNegotiationState + "400": + description: Request was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/dataplanes: + get: + tags: + - Dataplane Selector + description: Returns a list of all currently registered data plane instances + operationId: getAll + responses: + "204": + description: A (potentially empty) list of currently registered data plane + instances + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + post: + tags: + - Dataplane Selector + description: Adds one datatplane instance to the internal database of the selector + operationId: addEntry + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DataPlaneInstanceSchema' + responses: + "200": + description: Entry was added successfully to the database + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/dataplanes/select: + post: + tags: + - Dataplane Selector + description: Finds the best fitting data plane instance for a particular query + operationId: find_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SelectionRequestSchema' + responses: + "200": + description: The DataPlane instance that fits best for the given selection + request + content: + application/json: + schema: + $ref: '#/components/schemas/DataPlaneInstanceSchema' + "204": + description: No suitable DataPlane instance was found + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/policydefinitions: + post: + tags: + - Policy Definition + description: Creates a new policy definition + operationId: createPolicyDefinition + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyDefinitionInput' + responses: + "200": + description: policy definition was created successfully. Returns the Policy + Definition Id and created timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "Could not create policy definition, because a contract definition\ + \ with that ID already exists" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/policydefinitions/request: + post: + tags: + - Policy Definition + description: Returns all policy definitions according to a query + operationId: queryPolicyDefinitions + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The policy definitions matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/PolicyDefinitionOutput' + "400": + description: Request was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/policydefinitions/{id}: + get: + tags: + - Policy Definition + description: Gets a policy definition with the given ID + operationId: getPolicyDefinition + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The policy definition + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyDefinitionOutput' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An policy definition with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + put: + tags: + - Policy Definition + description: "Updates an existing Policy, If the Policy is not found, an error\ + \ is reported" + operationId: updatePolicyDefinition + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyDefinitionInput' + responses: + "200": + description: policy definition was updated successfully. Returns the Policy + Definition Id and updated timestamp + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: "policy definition could not be updated, because it does not\ + \ exists" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDetail' + delete: + tags: + - Policy Definition + description: "Removes a policy definition with the given ID if possible. Deleting\ + \ a policy definition is only possible if that policy definition is not yet\ + \ referenced by a contract definition, in which case an error is returned.\ + \ DANGER ZONE: Note that deleting policy definitions can have unexpected results,\ + \ do this at your own risk!" + operationId: deletePolicyDefinition + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Policy definition was deleted successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An policy definition with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "The policy definition cannot be deleted, because it is referenced\ + \ by a contract definition" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/transferprocesses: + post: + tags: + - Transfer Process + description: "Initiates a data transfer with the given parameters. Please note\ + \ that successfully invoking this endpoint only means that the transfer was\ + \ initiated. Clients must poll the /{id}/state endpoint to track the state" + operationId: initiateTransferProcess + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TransferRequest' + responses: + "200": + description: The transfer was successfully initiated. Returns the transfer + process ID and created timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + links: + poll-state: + operationId: getTransferProcessState + parameters: + id: $response.body#/id + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/transferprocesses/request: + post: + tags: + - Transfer Process + description: Returns all transfer process according to a query + operationId: queryTransferProcesses + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The transfer processes matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/TransferProcess' + "400": + description: Request was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/transferprocesses/{id}: + get: + tags: + - Transfer Process + description: Gets an transfer process with the given ID + operationId: getTransferProcess + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The transfer process + content: + application/json: + schema: + $ref: '#/components/schemas/TransferProcess' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A transfer process with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/transferprocesses/{id}/deprovision: + post: + tags: + - Transfer Process + description: "Requests the deprovisioning of resources associated with a transfer\ + \ process. Due to the asynchronous nature of transfers, a successful response\ + \ only indicates that the request was successfully received. This may take\ + \ a long time, so clients must poll the /{id}/state endpoint to track the\ + \ state." + operationId: deprovisionTransferProcess + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Request to deprovision the transfer process was successfully + received + links: + poll-state: + operationId: getTransferProcessState + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/transferprocesses/{id}/state: + get: + tags: + - Transfer Process + description: Gets the state of a transfer process with the given ID + operationId: getTransferProcessState + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The transfer process's state + content: + application/json: + schema: + $ref: '#/components/schemas/TransferState' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An transfer process with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v2/transferprocesses/{id}/terminate: + post: + tags: + - Transfer Process + description: "Requests the termination of a transfer process. Due to the asynchronous\ + \ nature of transfers, a successful response only indicates that the request\ + \ was successfully received. Clients must poll the /{id}/state endpoint to\ + \ track the state." + operationId: terminateTransferProcess + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TerminateTransfer' + responses: + "200": + description: Request to cancel the transfer process was successfully received + links: + poll-state: + operationId: getTransferProcessState + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: A contract negotiation with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "Could not terminate transfer process, because it is already\ + \ completed or terminated." + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v3/assets: + put: + tags: + - Asset + description: "Updates an asset with the given ID if it exists. If the asset\ + \ is not found, no further action is taken. DANGER ZONE: Note that updating\ + \ assets can have unexpected results, especially for contract offers that\ + \ have been sent out or are ongoing in contract negotiations." + operationId: updateAsset_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AssetInput' + responses: + "200": + description: Asset was updated successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: "Asset could not be updated, because it does not exist." + post: + tags: + - Asset + description: Creates a new asset together with a data address + operationId: createAsset_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AssetInput' + responses: + "200": + description: Asset was created successfully. Returns the asset Id and created + timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "Could not create asset, because an asset with that ID already\ + \ exists" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v3/assets/request: + post: + tags: + - Asset + description: ' all assets according to a particular query' + operationId: requestAssets_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpec' + responses: + "200": + description: The assets matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/AssetOutput' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v3/assets/{id}: + get: + tags: + - Asset + description: Gets an asset with the given ID + operationId: getAsset_1 + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The asset + content: + application/json: + schema: + $ref: '#/components/schemas/AssetOutput' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + delete: + tags: + - Asset + description: "Removes an asset with the given ID if possible. Deleting an asset\ + \ is only possible if that asset is not yet referenced by a contract agreement,\ + \ in which case an error is returned. DANGER ZONE: Note that deleting assets\ + \ can have unexpected results, especially for contract offers that have been\ + \ sent out or ongoing or contract negotiations." + operationId: removeAsset_1 + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Asset was deleted successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "The asset cannot be deleted, because it is referenced by a\ + \ contract agreement" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' +components: + schemas: + ApiErrorDetail: + type: object + properties: + invalidValue: + type: object + example: null + message: + type: string + example: null + path: + type: string + example: null + type: + type: string + example: null + example: null + Asset: + type: object + properties: + createdAt: + type: integer + format: int64 + example: null + dataAddress: + $ref: '#/components/schemas/DataAddress' + id: + type: string + example: null + privateProperties: + type: object + additionalProperties: + type: object + example: null + example: null + properties: + type: object + additionalProperties: + type: object + example: null + example: null + example: null + AssetEntryNewDto: + type: object + properties: + asset: + $ref: '#/components/schemas/Asset' + dataAddress: + $ref: '#/components/schemas/DataAddress' + example: null + AssetInput: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/Asset + dataAddress: + $ref: '#/components/schemas/DataAddress' + privateProperties: + type: object + additionalProperties: + type: object + example: null + example: null + properties: + type: object + additionalProperties: + type: object + example: null + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': asset-id + properties: + key: value + privateProperties: + privateKey: privateValue + dataAddress: + type: HttpData + AssetOutput: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/Asset + createdAt: + type: integer + format: int64 + example: null + dataAddress: + $ref: '#/components/schemas/DataAddress' + privateProperties: + type: object + additionalProperties: + type: object + example: null + example: null + properties: + type: object + additionalProperties: + type: object + example: null + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': asset-id + edc:properties: + edc:key: value + edc:privateProperties: + edc:privateKey: privateValue + edc:dataAddress: + edc:type: HttpData + edc:createdAt: 1688465655 + CallbackAddress: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/CallbackAddress + authCodeId: + type: string + example: null + authKey: + type: string + example: null + events: + uniqueItems: true + type: array + example: null + items: + type: string + example: null + transactional: + type: boolean + example: null + uri: + type: string + example: null + example: null + Catalog: + type: object + description: DCAT catalog + example: + '@id': 7df65569-8c59-4013-b1c0-fa14f6641bf2 + '@type': dcat:Catalog + dcat:dataset: + '@id': bcca61be-e82e-4da6-bfec-9716a56cef35 + '@type': dcat:Dataset + odrl:hasPolicy: + '@id': OGU0ZTMzMGMtODQ2ZS00ZWMxLThmOGQtNWQxNWM0NmI2NmY4:YmNjYTYxYmUtZTgyZS00ZGE2LWJmZWMtOTcxNmE1NmNlZjM1:NDY2ZTZhMmEtNjQ1Yy00ZGQ0LWFlZDktMjdjNGJkZTU4MDNj + '@type': odrl:Set + odrl:permission: + odrl:target: bcca61be-e82e-4da6-bfec-9716a56cef35 + odrl:action: + odrl:type: http://www.w3.org/ns/odrl/2/use + odrl:constraint: + odrl:and: + - odrl:leftOperand: https://w3id.org/edc/v0.0.1/ns/inForceDate + odrl:operator: + '@id': odrl:gteq + odrl:rightOperand: 2023-07-07T07:19:58.585601395Z + - odrl:leftOperand: https://w3id.org/edc/v0.0.1/ns/inForceDate + odrl:operator: + '@id': odrl:lteq + odrl:rightOperand: 2023-07-12T07:19:58.585601395Z + odrl:prohibition: [ ] + odrl:obligation: [ ] + odrl:target: bcca61be-e82e-4da6-bfec-9716a56cef35 + dcat:distribution: + - '@type': dcat:Distribution + dct:format: + '@id': HttpData + dcat:accessService: 5e839777-d93e-4785-8972-1005f51cf367 + edc:description: description + edc:id: bcca61be-e82e-4da6-bfec-9716a56cef35 + dcat:service: + '@id': 5e839777-d93e-4785-8972-1005f51cf367 + '@type': dcat:DataService + dct:terms: connector + dct:endpointUrl: http://localhost:16806/protocol + edc:participantId: urn:connector:provider + '@context': + dct: https://purl.org/dc/terms/ + edc: https://w3id.org/edc/v0.0.1/ns/ + dcat: https://www.w3.org/ns/dcat/ + odrl: http://www.w3.org/ns/odrl/2/ + dspace: https://w3id.org/dspace/v0.8/ + CatalogRequest: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/CatalogRequest + counterPartyAddress: + type: string + example: null + protocol: + type: string + example: null + providerUrl: + type: string + description: please use counterPartyAddress instead + example: null + deprecated: true + querySpec: + $ref: '#/components/schemas/QuerySpec' + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': CatalogRequest + counterPartyAddress: http://provider-address + protocol: dataspace-protocol-http + querySpec: + offset: 0 + limit: 50 + sortOrder: DESC + sortField: fieldName + filterExpression: [ ] + ContractAgreement: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/ContractAgreement + assetId: + type: string + example: null + consumerId: + type: string + example: null + contractSigningDate: + type: integer + format: int64 + example: null + policy: + $ref: '#/components/schemas/Policy' + providerId: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/ContractAgreement + '@id': negotiation-id + providerId: provider-id + consumerId: consumer-id + assetId: asset-id + contractSigningDate: 1688465655 + policy: + '@context': http://www.w3.org/ns/odrl.jsonld + '@type': Set + '@id': offer-id + permission: + - target: asset-id + action: display + ContractDefinitionInput: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/ContractDefinition + accessPolicyId: + type: string + example: null + assetsSelector: + type: array + example: null + items: + $ref: '#/components/schemas/Criterion' + contractPolicyId: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': definition-id + accessPolicyId: asset-policy-id + contractPolicyId: contract-policy-id + assetsSelector: [ ] + ContractDefinitionOutput: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/ContractDefinition + accessPolicyId: + type: string + example: null + assetsSelector: + type: array + example: null + items: + $ref: '#/components/schemas/Criterion' + contractPolicyId: + type: string + example: null + createdAt: + type: integer + format: int64 + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': definition-id + edc:accessPolicyId: asset-policy-id + edc:contractPolicyId: contract-policy-id + edc:assetsSelector: [ ] + edc:createdAt: 1688465655 + ContractNegotiation: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/ContractNegotiation + callbackAddresses: + type: array + example: null + items: + $ref: '#/components/schemas/CallbackAddress' + contractAgreementId: + type: string + example: null + counterPartyAddress: + type: string + example: null + counterPartyId: + type: string + example: null + errorDetail: + type: string + example: null + protocol: + type: string + example: null + state: + type: string + example: null + type: + type: string + example: null + enum: + - CONSUMER + - PROVIDER + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/ContractNegotiation + '@id': negotiation-id + type: PROVIDER + protocol: dataspace-protocol-http + counterPartyId: counter-party-id + counterPartyAddress: http://counter/party/address + state: VERIFIED + contractAgreementId: contract:agreement:id + errorDetail: eventual-error-detail + createdAt: 1688465655 + callbackAddresses: + - transactional: false + uri: http://callback/url + events: + - contract.negotiation + - transfer.process + authKey: auth-key + authCodeId: auth-code-id + ContractOfferDescription: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/ContractOfferDescription + assetId: + type: string + example: null + offerId: + type: string + example: null + policy: + $ref: '#/components/schemas/Policy' + example: null + ContractRequest: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/ContractRequest + callbackAddresses: + type: array + example: null + items: + $ref: '#/components/schemas/CallbackAddress' + connectorAddress: + type: string + example: null + connectorId: + type: string + description: please use providerId instead + example: null + deprecated: true + consumerId: + type: string + description: this field is not used anymore + example: null + deprecated: true + offer: + $ref: '#/components/schemas/ContractOfferDescription' + protocol: + type: string + example: null + providerId: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/ContractRequest + connectorAddress: http://provider-address + protocol: dataspace-protocol-http + providerId: provider-id + offer: + offerId: offer-id + assetId: asset-id + policy: + '@context': http://www.w3.org/ns/odrl.jsonld + '@type': Set + '@id': offer-id + permission: + - target: asset-id + action: display + callbackAddresses: + - transactional: false + uri: http://callback/url + events: + - contract.negotiation + - transfer.process + authKey: auth-key + authCodeId: auth-code-id + Criterion: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/Criterion + operandLeft: + type: object + example: null + operandRight: + type: object + example: null + operator: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': Criterion + operandLeft: fieldName + operator: = + operandRight: some value + DataAddress: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/DataAddress + type: + type: string + example: null + example: null + DataPlaneInstanceSchema: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/DataPlaneInstance + allowedDestTypes: + uniqueItems: true + type: array + example: null + items: + type: string + example: null + allowedSourceTypes: + uniqueItems: true + type: array + example: null + items: + type: string + example: null + lastActive: + type: integer + format: int64 + example: null + turnCount: + type: integer + format: int32 + example: null + url: + type: string + format: url + example: null + example: + '@id': your-dataplane-id + url: http://somewhere.com:1234/api/v1 + allowedSourceTypes: + - source-type1 + - source-type2 + allowedDestTypes: + - your-dest-type + Dataset: + type: object + description: DCAT dataset + example: + '@id': bcca61be-e82e-4da6-bfec-9716a56cef35 + '@type': dcat:Dataset + odrl:hasPolicy: + '@id': OGU0ZTMzMGMtODQ2ZS00ZWMxLThmOGQtNWQxNWM0NmI2NmY4:YmNjYTYxYmUtZTgyZS00ZGE2LWJmZWMtOTcxNmE1NmNlZjM1:NDY2ZTZhMmEtNjQ1Yy00ZGQ0LWFlZDktMjdjNGJkZTU4MDNj + '@type': odrl:Set + odrl:permission: + odrl:target: bcca61be-e82e-4da6-bfec-9716a56cef35 + odrl:action: + odrl:type: http://www.w3.org/ns/odrl/2/use + odrl:constraint: + odrl:and: + - odrl:leftOperand: https://w3id.org/edc/v0.0.1/ns/inForceDate + odrl:operator: + '@id': odrl:gteq + odrl:rightOperand: 2023-07-07T07:19:58.585601395Z + - odrl:leftOperand: https://w3id.org/edc/v0.0.1/ns/inForceDate + odrl:operator: + '@id': odrl:lteq + odrl:rightOperand: 2023-07-12T07:19:58.585601395Z + odrl:prohibition: [ ] + odrl:obligation: [ ] + odrl:target: bcca61be-e82e-4da6-bfec-9716a56cef35 + dcat:distribution: + - '@type': dcat:Distribution + dct:format: + '@id': HttpData + dcat:accessService: 5e839777-d93e-4785-8972-1005f51cf367 + edc:description: description + edc:id: bcca61be-e82e-4da6-bfec-9716a56cef35 + '@context': + dct: https://purl.org/dc/terms/ + edc: https://w3id.org/edc/v0.0.1/ns/ + dcat: https://www.w3.org/ns/dcat/ + odrl: http://www.w3.org/ns/odrl/2/ + dspace: https://w3id.org/dspace/v0.8/ + DatasetRequest: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/CatalogRequest + counterPartyAddress: + type: string + example: null + protocol: + type: string + example: null + querySpec: + $ref: '#/components/schemas/QuerySpec' + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': DatasetRequest + '@id': dataset-id + counterPartyAddress: http://counter-party-address + protocol: dataspace-protocol-http + DeprovisionedResource: + type: object + properties: + error: + type: boolean + example: null + errorMessage: + type: string + example: null + inProcess: + type: boolean + example: null + provisionedResourceId: + type: string + example: null + example: null + Failure: + type: object + properties: + failureDetail: + type: string + example: null + messages: + type: array + example: null + items: + type: string + example: null + example: null + HealthCheckResult: + type: object + properties: + component: + type: string + example: null + failure: + $ref: '#/components/schemas/Failure' + isHealthy: + type: boolean + example: null + example: null + HealthStatus: + type: object + properties: + componentResults: + type: array + example: null + items: + $ref: '#/components/schemas/HealthCheckResult' + isSystemHealthy: + type: boolean + example: null + example: null + IdResponse: + type: object + properties: + '@id': + type: string + example: null + createdAt: + type: integer + format: int64 + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': id-value + createdAt: 1688465655 + JsonArray: + type: array + properties: + empty: + type: boolean + example: null + valueType: + type: string + example: null + enum: + - ARRAY + - OBJECT + - STRING + - NUMBER + - "TRUE" + - "FALSE" + - "NULL" + example: null + items: + $ref: '#/components/schemas/JsonValue' + JsonObject: + type: object + properties: + empty: + type: boolean + example: null + valueType: + type: string + example: null + enum: + - ARRAY + - OBJECT + - STRING + - NUMBER + - "TRUE" + - "FALSE" + - "NULL" + additionalProperties: + $ref: '#/components/schemas/JsonValue' + example: null + JsonValue: + type: object + properties: + valueType: + type: string + example: null + enum: + - ARRAY + - OBJECT + - STRING + - NUMBER + - "TRUE" + - "FALSE" + - "NULL" + example: null + NegotiationState: + type: object + properties: + state: + type: string + example: null + example: null + Policy: + type: object + description: ODRL policy + example: + '@context': http://www.w3.org/ns/odrl.jsonld + '@id': 0949ba30-680c-44e6-bc7d-1688cbe1847e + '@type': odrl:Set + permission: + target: http://example.com/asset:9898.movie + action: + type: http://www.w3.org/ns/odrl/2/use + constraint: + leftOperand: https://w3id.org/edc/v0.0.1/ns/left + operator: eq + rightOperand: value + prohibition: [ ] + obligation: [ ] + PolicyDefinitionInput: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/PolicyDefinition + policy: + $ref: '#/components/schemas/Policy' + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': definition-id + policy: + '@context': http://www.w3.org/ns/odrl.jsonld + '@type': Set + uid: http://example.com/policy:1010 + permission: + - target: http://example.com/asset:9898.movie + action: display + constraint: + - leftOperand: spatial + operator: eq + rightOperand: https://www.wikidata.org/wiki/Q183 + comment: i.e Germany + PolicyDefinitionOutput: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/PolicyDefinition + policy: + $ref: '#/components/schemas/Policy' + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@id': definition-id + policy: + '@context': http://www.w3.org/ns/odrl.jsonld + '@type': Set + uid: http://example.com/policy:1010 + permission: + - target: http://example.com/asset:9898.movie + action: display + constraint: + - leftOperand: spatial + operator: eq + rightOperand: https://www.wikidata.org/wiki/Q183 + comment: i.e Germany + createdAt: 1688465655 + ProvisionerWebhookRequest: + type: object + properties: + apiKeyJwt: + type: string + example: null + assetId: + type: string + example: null + contentDataAddress: + $ref: '#/components/schemas/DataAddress' + hasToken: + type: boolean + example: null + resourceDefinitionId: + type: string + example: null + resourceName: + type: string + example: null + example: null + QuerySpec: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/QuerySpec + filterExpression: + type: array + example: null + items: + $ref: '#/components/schemas/Criterion' + limit: + type: integer + format: int32 + example: null + offset: + type: integer + format: int32 + example: null + sortField: + type: string + example: null + sortOrder: + type: string + example: null + enum: + - ASC + - DESC + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': QuerySpec + offset: 5 + limit: 10 + sortOrder: DESC + sortField: fieldName + filterExpression: [ ] + SelectionRequestSchema: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/SelectionRequest + destination: + $ref: '#/components/schemas/DataAddress' + source: + $ref: '#/components/schemas/DataAddress' + strategy: + type: string + example: null + example: + source: + '@type': https://w3id.org/edc/v0.0.1/ns/DataAddress + type: test-src1 + destination: + '@type': https://w3id.org/edc/v0.0.1/ns/DataAddress + type: test-dst2 + strategy: you_custom_strategy + TerminateNegotiationSchema: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/TerminateNegotiation + reason: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/TerminateNegotiation + '@id': negotiation-id + reason: a reason to terminate + TerminateTransfer: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/TransferState + state: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/TerminateTransfer + reason: a reason to terminate + TransferProcess: + type: object + properties: + '@id': + type: string + example: null + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/TransferProcess + callbackAddresses: + type: array + example: null + items: + $ref: '#/components/schemas/CallbackAddress' + contractAgreementId: + type: string + example: null + counterPartyAddress: + type: string + example: null + counterPartyId: + type: string + example: null + errorDetail: + type: string + example: null + privateProperties: + type: object + additionalProperties: + type: object + example: null + example: null + properties: + type: object + additionalProperties: + type: string + example: null + deprecated: true + example: null + deprecated: true + protocol: + type: string + example: null + state: + type: string + example: null + type: + type: string + example: null + enum: + - CONSUMER + - PROVIDER + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/TransferProcess + '@id': process-id + correlationId: correlation-id + type: PROVIDER + state: STARTED + stateTimestamp: 1688465655 + assetId: asset-id + connectorId: connectorId + contractId: contractId + dataDestination: + type: data-destination-type + privateProperties: + private-key: private-value + errorDetail: eventual-error-detail + createdAt: 1688465655 + callbackAddresses: + - transactional: false + uri: http://callback/url + events: + - contract.negotiation + - transfer.process + authKey: auth-key + authCodeId: auth-code-id + TransferRequest: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/TransferRequest + assetId: + type: string + example: null + callbackAddresses: + type: array + example: null + items: + $ref: '#/components/schemas/CallbackAddress' + connectorAddress: + type: string + example: null + connectorId: + type: string + example: null + contractId: + type: string + example: null + dataDestination: + $ref: '#/components/schemas/DataAddress' + privateProperties: + type: object + additionalProperties: + type: string + example: null + example: null + properties: + type: object + additionalProperties: + type: string + description: "Deprecated as this field is not used anymore, please use\ + \ privateProperties instead" + example: null + deprecated: true + description: "Deprecated as this field is not used anymore, please use privateProperties\ + \ instead" + example: null + deprecated: true + protocol: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/TransferRequest + protocol: dataspace-protocol-http + connectorAddress: http://provider-address + connectorId: provider-id + contractId: contract-id + assetId: asset-id + dataDestination: + type: data-destination-type + privateProperties: + private-key: private-value + callbackAddresses: + - transactional: false + uri: http://callback/url + events: + - contract.negotiation + - transfer.process + authKey: auth-key + authCodeId: auth-code-id + TransferState: + type: object + properties: + '@type': + type: string + example: https://w3id.org/edc/v0.0.1/ns/TransferState + state: + type: string + example: null + example: + '@context': + edc: https://w3id.org/edc/v0.0.1/ns/ + '@type': https://w3id.org/edc/v0.0.1/ns/TransferState + state: STARTED diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md new file mode 100644 index 000000000..e9d6fc4c7 --- /dev/null +++ b/docs/getting-started/README.md @@ -0,0 +1,12 @@ +Getting Started +======== + +### Quick Start + +For quickly starting two sovity EDC CE or MDS EDC CE Connectors locally, please check out +our [Deployment Goal: Local Demo](../deployment-guide/goals/local-demo). + +### Productive Deployments + +For deploying productive EDC Connectors with a fully-fledged Data Space Roll-In, please refer to +our [Deployment Goal: Production](../deployment-guide/goals/production). diff --git a/docs/getting-started/documentation/api_wrapper.md b/docs/getting-started/documentation/api_wrapper.md new file mode 100644 index 000000000..5a79fd9d7 --- /dev/null +++ b/docs/getting-started/documentation/api_wrapper.md @@ -0,0 +1,45 @@ +Manging a sovity EDC Connector via the API Wrapper Java Client Library +======== + +Introduction to the sovity EDC API Wrapper +======== +The sovity EDC API Wrapper contains several APIs, of which some are available in either our sovity EDC CE or our sovity +CE EE / Connector-as-a-Servcie (CaaS). These APIs are made accessible via type-safe generated client libraries. Please +note that most of these APIs are not yet complete and are under development: + +- **Use Case API**: Generic API for Use Case Applications. Its goal is to replace the Management API, so there can be + stable endpoints across milestones in the auto-generated client libraries. It's still in development, so expect many + new endpoints to be added here in the near future. +- **UI API**: API endpoints for the sovity EDC UI: These endpoints might contain interesting data, that a Use Case + Application might profit from, but expect these endpoints to be unstable and subject to change. +- **Enterprise Edition API**: Special API endpoint only available in the Connector-as-a-Service (CaaS). Features such as + File Storage are currently in development, but to be expected in the near future. + +Using the Java Client Library +======== +This requires JDK11 or higher, and either a Gradle or Maven project. + +Installing The Java Client Library +======== +Connect your Maven or Gradle Project to the Github Maven Registry + +- +Maven: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry#authenticating-to-github-packages +- +Gradle: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages +- This might require a Github Personal Access Token (PAT) + Add the Java Client Library to your Maven/Gradle project: https://github.com/sovity/edc-extensions/packages/1825774 + +Configuring The Client +======== + +- Configure the Client with either an API Key or OAuth2 Client + Credentials: https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper/clients/java-client#usage +- Your management API URL should look like https://your-connector-name.prod-sovity.azure.sovity.io/control/data + +Using The Client +======== +Feel free to use the endpoints of the aforementioned API groups. + +A full example providing and consuming a data offer using the API Wrapper Client Library can be found +in [ApiWrapperDemoTest.java](../../../tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java). diff --git a/docs/getting-started/documentation/data-transfer-methods.md b/docs/getting-started/documentation/data-transfer-methods.md new file mode 100644 index 000000000..47cad3f25 --- /dev/null +++ b/docs/getting-started/documentation/data-transfer-methods.md @@ -0,0 +1,15 @@ +Which data transfer methods are supported by the EDC-Connector? +======== + +The connector supports three different data transfer modes: + +1. HTTPData: The provider EDC fetches the data from its own backend and pushes it to the consumer's desired data sink. +2. HTTPProxy: The provider EDC fetches the data and passes it on consumer's data transfer request synchronously back to the consumer. + +The following diagram illustrates the different transmission modes: +![data-transfer-methods.png](images%2Fdata-transfer-methods.png) + +# Consuming Data via HttpProxy / HTTP Pull +The Use-Case Backend-Application is involved in steps b1, b4, b5 and b8 of the diagram. It should provide an endpoint for receiving +the EDR (b4). These information can then be used to start the tranfser request (b5). The result of the transfer request +will contain the data (b8). Please see related [documentation](./pull-data-transfer.md) about how to implement it technically. diff --git a/docs/getting-started/documentation/images/data-transfer-methods.png b/docs/getting-started/documentation/images/data-transfer-methods.png new file mode 100644 index 0000000000000000000000000000000000000000..3c41a1e1911c4e40d9342c699ef8b2a9d23a3e28 GIT binary patch literal 2507761 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 H)@=w4$ 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%0Aq})$v2&elM~{kizSar{dPI(W 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

vI$7QJ901j#e6do+nCbe-KDek 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 ALtLG3%NRdpf8cFtvkT`t|ws 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

q!!Pd|V_tq1Mee$}*7(e|s5Z)7-cDCw~1L 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

Lw4_lqFE8?WnMdigunYnRmPSLgU*Iw_o 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 W$&KR^utYRA zlss~yO!yU*U{qS}MvFifTG6zng}W4N4BjrS?hggd(Lf@sAcia>!f=jnUY;+VIUYn9u<{6YQv!}1N$1DaHR4N1MZ)zW=zB)I$yeh z6T1)JEi)a^K&>Anr8rJC&S}q>mD?^ZpV}RpPSh1Tz>bY?>JUV{8R=lK`&c4gcm&Yp zI*{tA)0JDA0Fihj@eMN2oOgz%OL(HW4I=Uv8?rV*6FKH_nNnnw^N4qWO=^?e77AvpD5bLJ>%3WsXP$siDZ}6l z)1$YeMVOln=bd03E;KJk@OEP7FbN0Ijj>d+pYLHF%ca&(^+nsdj+)1oO*sP8#VQ(s znc5EehF@XyXkgwhPy@R3@E(+g43+7(!`VaM^8Kp+eak6uq3W=9tj3|Ye0bD(NAHB* zy857!Re6a+_!q78!yXTxwL)-8`ybTpsT2e9HA4&kOOFlNZnZ_T4vZ83J7hjt}? zJlak%5dR%wHSynPI&t!`1$??RFIRk*(6-$A2mY+R3-DY|)XSL@s;AWEN#E*#n0T%O zD`~C{Xx0heT~zde{=N+(qD}a8c>$%V?JAQL=XjCjM3~Xrj~3Q1ERtCO zVyO;J$9k%>y>gi#!0awhD2Yu#Lq~g8o5;jwRp$lzaQmlu+}d0}OUF*zkwOSSOLUP-9>rV^zP7EB4TQPsOv*Q= zg{RJg%+iA}30F86+=%anVTENs=uVNid9U?3wxBkW zLQw+AAdb>VD;Hp_7XB){M~gj7qP4Z8q*HvoR8zEsS#-u|&f|Gca@>zBkKS|ccD%Gn zCHIHNZooQ39`@VkbVIiFfcAx~X2}#!R^xe9@tr^)e3Zl(fl|Su{KbZ@dqUW1Br`*# zbvx*}cJ08g+cxJ(RzPzSU|v^x9!8{zdE#^?6n=pzm28wqSWq05=_6qCl?20?nA;R% zo$1?RsWR=c@AwsS;W8M$6?(OaF6EcOauF;lXTEiWsTav}a}Wa*mCz8iuX;pzqIb5% z2~6}OZv7#>%dw}Enjw!P20z4GerfO;!ywv_`S;^y!p~k@%m456^*7w$=J_SG#KVya#tP&VAkGfqw{Kxmx(N?QJwx1WtT! zctzi2{KpGWYxN4*#~$$7+z1U2GB#|iWDdu%+Ahy#AKTO6)kc`3q@FDLD-8E{b58 zJP3_hH(Q#*(QB~B(vcquD%>KB4vjhN=KfYbA?aI(&JavkFzS%jIRj4tKa@`2WawiM z?lVEEsq$`5B$)pbCkMTQxee>txNN8@9EkZ9kNp_ib6&O_M^1u|F)I=tl`OO1ZS~w^ z&Tw+BYqAIOL{X#bNgk&M%y^d8#QRd;;KffxOQv6AM3>AKSI;KR1Hp#_OE3s_IXLY; zzhx~Yi3n@@lp)tUzAmUY197c zZD(Lt8qTf^>)k}Z4jcaZxL*tfC-2~VUtvmP@)D(X;DU<47Wh5V9#;3xmN4R`m)@`e z=XvspM3JWwrWROB0w?!4Ikw!99PGg@pLfS6FF})b7N{G zzU>}e1Qm?T9gO<(j9bK!5|DG%Y?zn!QE?Z{hW}*8JiF>BuX$}&GDw6qI!q;!TAxmJ z0ya&{vB2a^4!4Z!uLQd4O?j_>hTogb+cwF5uPofMXVxHuVw;2PD_Y@{?7f221~5Nj zuFS^BiZ9OA%3v(N!-x4K^uagrETddyF2xK7f$TKpth4vqZr5bzmG%1t&o(P8YmMu3 zU(F3v3f~Zx&lzJix?6+Ir`_Y-)}?_8t?O4^_sB5qsSwnQB!202aiSz$K-gZ(cJxM% zuM-NtZezf^G)^AIVmIQ2pN|XkdCqH#*$Wjb@ZWZYS_YBj{;1C|(*FXFnjHr3kVuKB zmM;2Nav^RDe#`xn6XAgz3{tT~Vs%mJR2x}9jH=#EmDl=Ar!%6Fn z$O@C72)cVZy39?ZAgzySo=4e}O_J@_DH`y_QxPY^nR1>1%Y#70bI+OXhR{sX#jR z8VLpE7n9e-%16`<5TYZTAY^dAaRuRk+h!F3h=LvV(ZA!`_!g2*gq^TUTc)_>EbB(F zdst3v%0f`Jw!*6d*{|T<*KsPKHINoJiKSL=FYNV&i zrg>i-0Ng_XGpr)O6fa43w~5KiDGOWwYAL(2-!U)s%zKAlnDpqJ2#jw6aYw@>SWt>5|;J%Ykb0naAUQz>&bE6LU% zAw!SEq|jU>aZFS&bFe-a=TOr1vz#+^6wAk|;cEeM;)Gto2H$s0IeT@P9IV5@qXmBL1ETd~|OIM%rLu{ zrF-%rMBgjupOHxm>9%x1H#I`>FF)4EG|5>jt(q<>xaVWEztk&DWlGclI!R^Mu2b%mT7 z0K}&_zO{c>sf_jh4m(!!Iz3Cq*LTXz7X!-;5)eXxJOJSW>8G@D9N@Np6s+Ztqf8_ z!(>@35mna)S(i^%jm@KK7T9W=r&rGgdv%B8E67!vd*%x>^yn_E!4PO-JDoM2Uw#y( z>hXe+U{H{gQy*e~o^zFe_I&tMMwspr7$C>W$?LwK2ujpg7u6?*!!M^1? z0t;60jC&b)ZaNkDS`5{iE5{)oF_G}j0{eyd#60tKmSxb!>m;Z0AFu0QQRpr2U-$$* zC#ff;FPhB5o0r7Qb%+Tx#dU4v!~jiu5t`F?3r;1H7CJQDpKBf^jG4*&>0yMq+l1KNcB0+ZwBNugIXneeo9IH zEN!C+u$hX&ez7M!)FMD59~O@<=(v1{-6C*Q+Ogz7fMk8L^QR_GjI3x|*|vvo#w)La zLMHn5BRz~SX}%V%C}4&2!F5$PYXxB@&`ib6ZRzbi5lQG$Ez3S+X}M=mEH@P9Ai;VJ zW%1A39uZ6S(F~g76*xByWOM;TkEpT*!1c4wZF9%oBB&yCy}?l_d9M&iabvn7{nDzU(#*!&TA=536vwqpkRu@0!cJ6D@%Hfe9LZ&z{=EQjfA^bzn|zfM1M2V|nCUuG&t(j0*O-@$8s*ot(iCk2K! zCziu1x{;j_FA3AylbPwA?I)3p82V%=OFRUZf~2ZdQ4zA6I!*dS#_brVlA5!UHmP_c zmhv0X>%B8fS0rGP>Uj6KM868pH(xqbwIP@~Fn>p2{e;(BK_)E4YUx(+(l@`n<9g3H zPxcJTddB&X+bdYz_bCdsa(Py;Zoa z13OjVNeX*2j-_LKxyDkH?9oKU*^dYnIGn9}oW~&RadPP~r?5RK$kDPX*02(67up>M zK{-fl88;{4_oHFm>N%qKODqdN0zsnky44lk-JlQgX9*cV{-6N%_F~=wx{%nqNln8$ zibyYt3a|?v!iZi~$n@!({wWN=OO1l0zbo0|Lwr*($(f8 z0oaWF@FDI!&ytG)UXWR{vV82{(vWEts1hzPOfjZ5QcGbov|M6q^Sd@GOgVI?py2q8 zM6sQ#k@0&X9_eQX5_i!mQx%L1Txi1auRsy4j}}y3QobL42y!%s+%2igpqH-P9L}bB zitL`Z&)|iX#fF;tv@|-!R|HFf+k?j{2W|juOdiA{CBM9hln1`mYVUq^eHr*B6ql=R zpCqID62Nd5`s5Die7PddV4TYyZRchQq!K*E6WYfu#>mG-G%vaKGJu%Xa^*pLc4>5k zsgSyze_w~jJ%W>mLNR)=iVC!1CGOHm`(VeUsDRt)+l)Zw5sWZVAEBKScH93HEVYYo zQY1X#pJ2E6BKjs~`6f$ds-;$)yc|aEf2|g1=D8w{FPq^BWK%qO96$5*ei#Wb&oa%1 zQ!@Hj77Vl#^7yM0N?4Zwzr0}^et*?37w3jfwSS(p|I-#tA}J+w#nZDdp=Fz~rOx3q|N8o5WsbzM4y+b$-Uj*nICbt{=C9d-R-L3>F zQ=$1fG*M2S%PcCeOqMIKSLBSSM{ zqD(C*VqykDyB9z>Fo89;ZRDqnYUwF5$vE*44D?L-6WY>GbCi$kMLFDtda-J%2^tsygBRNr7Epit=@R7cN;8+TG zb3{B{&3dq3CY*OEMT$r!0L%n*HJJLt8*I-2#0hF>raq zL2i^Ti*m>IW95^lW+&7sZ#MUg-Nt_YIo@=X*XbT4m?yOrM8(7dE?rw_pb!vfS&;Dj zy7PHLd-r%SYitPYZTagw_%3s{0x^{zbHnQA`&%e$FMzIYwrmZX$`lJeZyOX)=ssXG816s1kIA6eruSVHZzzR@?2!LrOv# zB;@eic!y^K3eLW}AvhIpeWN29(h5WE<1g*a!&rvHtlX{PywPxi69H`3^cB3 zb`9E@n~ZpMx=qnl0?k7VoYp4rHQDq^pdf-Ha}q=sMu?gAj^}B3|3e_XAoVCJL(5m8 zC>hWkq6)hySGzuaKp#qBX5$s9HMC*Cv$b($J=|fwCDf5m2{?5SU5@%Pi!%I9 z1THK+3Mj^j{=rGYBh53C2lIDf`8`{_SL!E*o;GZAvxo8?t{HEY(YtmZf8%%QT6hKA zNycvgd$k-mrriw+MTl*pHX0oEK`atLha_7xioB`e+hzln$g_u+L3+*kWHsc6*K$;v z(SS)w<1$GiRwWLsi^#mDMVSF~L!)xu-V?wU5PgYR$-O5qxI^1JFkuwA;98Y_`BUFv zXqgXRD$%uXm&aM$KFYxJvwP-DYuNInVmJl#04u$YtscwdZ{6G{P7YI7av#}3^6a9EmAXC$a@93_Oo}VW>-L|u zxkY{!?xI)s9|2D)V?%^wgtsW%Q6es4EursIX6SZ`g%HEX8?E}%&RoFlN zNY6hk-*9u|K;U4T%Wd0~$c{^0VfAd$)}lizze~^{rHN|gZ585CMlq}=M~$eEt+=@5 z7#h{*K3??UMLX|Kx-24Rf`zcoijpzg*H}z6ukU8%s?&k?m~UUi&4_Y33C>mqLSWi#I^S>IY#uXu6_ zew25FHoYHMj%Q&{xqur&7%(vORL{Zy#|R`SQh|Q=m_;(xLw1X#2ON5)aC-^0FXnyb zHT)g0%J03|ijSEG8xF4*J-2F2;F~iFBA+0J566&^W7dfM**k!yFD4BH6b-m;Hb^Kcpc(#ZS;VLmsc=&aCn z51!Tufjcz{1anq;W@s_hVZzemSqlwlKKlo1$Zx6!91zI=$Ps z(a>ogmG(5C3I_oAp-~gf3yaxh4tvB{Yeca)0Gx6Y7$g?gBzZa0AJ_Jh%zom6$7FRJ z0-yySW1&V*#jOat%_zg$iPPS?kmojcqDOo{r_~l>yXkrANW}O5qh>3K|5Z47w;wz$ zfp@!`I&9CTX?ViT-`ua1*7@ZFl=P*d=8{Y2PX>cTfs_wc$)P!PLl%Wl{A(2O?7EnA=Ra4ih7ORHw7x4mZ66sMGn_ z!Hy(k#22$`FQ)N6QzS@hXkH=$B{regQj?CPlN2W8Gro;h1bqknG}WJrb6A6)??dPxdV7X1+5Jf4$}GyCJdw@P*Z zcef7aFnw44NE&*>Q7tAY>xUc{tz|gOxW+q~v$}%qPC=K^d0)_8 z9_hi@2TZHjkX~Vyl}#V5N{upYbeW!&&p#7+5VNWEWGSZOM?@v_G`EeZ@+{JFkm(_Y z&=Bt9u)azP*`G+G{C@xwq` z`stC3IF5uv3t8uMqPchzPB&LA>2vD)Qk7O^^p(cFTk1SLBQeJ zf7`+NvQfBRM$?bSxISRu1Qj6yl2RHnMj2kAPmW+b_q%_s1(!w~w)awmXD=sUvDA@3 zBKEr^FwM>Pc-8NeuRqxziP2*)JsLPKog}wEDr2jKGVq}4XVBV}8)TbpxYbUC^&EdC zOpS>j81SFcC&iK3R~cbrX7d3Tz|>wW^#?(MxKsm+ z@-YAaVa{sl4V+J$8>kbRm%8kn4qDIeXzn=kvuTECr($>20i$WBzo%c_evo#>2?Ec!$~?Q{ zn4VzNZo`j|JkJxb9AI6M1h<%bs*cyCU*6JqiY+~WRFTz@e)a1!EKrPsWjN(G8NyR4 zz>^B_0Q1U|C61c@^dtt5p3xX!56u|e`xpiy2`$|A@}^h2x_#3H<@^gOqfRYG)5(*K z%UaqVmYfEWV!qSpw~jNdC+9@w?R14m-l3(CMEwpg&^(EiDQ)elYg3oY8)o28?P z8a^Snh~0(@&Y2_zP0adbQwebE$pZk>_myE`yqp=~6#hSRPGiQ_MN~ z2hIB9j~+fM+DfshG^dY!`!o)L`VIDPf)=+OdDMuL#L zldP@mw?%Y9;23RZV`Ks1?8|IzY1T~c@nf*`?cVs2Nb824Ir&zvc>0IGn)uOE1gqv< zXpMMJ{KUyVX>(cer?&h`O((PVGUCYR_&2zdVATTfVlQ({SOoKx5{83#*Wi{&kt%CQ zAUDIyI_J=+8~!CB>T+##_@3;RN0ISnMk&*5+vEdhc~cUN`eTED?w0t61kXhR+<7#q zzKK5O3jsC-Y@U#X)5`j1HR|vwW3Xq*fzQfvnyFsIKqP`pII2oi3BDqFEQ%Ma*+z}U z-ly=_#KP;r(qb@!R&F}l^AXu-ap+2?HOYHUCW-w$#$z67R_QzbAP0N{dWM9-MA*$e zoUrfv?nFBudb+j`pFR`clkSyt zd|M2*zRwaA{LrM^=wZ8-Vc$`G0H%cp-|QidBE6aN%KwSeCT)`chB|A#syRK-1iOc12w-!3tc=RBiq2o|b?y*i|Na2AC!(vWp0TV$c5bKEmL87jJ9`>}$J?$QEJt!h( z5v%)#Ho|ql6YIHd4JD}apVi7g^zLW`nyfGLxr4oyk@fvBN%|<&YX<`(c!$Y z=vhb~v}w{)NPgSf^~?((g#|o2z2Oj7ASiUxkTH+B=E=jjK$z~1*u5;iNk2fCO z1aT#Gxp}&2XrR2y&&MjtsoU2n=7Kd>z!|r+EC0usHk4`$RLZ>Wh@JKX`HFMGcfnzy zRfwU8O*Fn9u(h5vnZ^(bPKaA_2~5tP)^v9;#@yI|9A3KNT+m0H;ZMKo+@D5LKhN)z zeOJK-oXOP@j*T58|5&{*X`-<*Mh7TOmk|<>Cdpb-po#=*izxs?_>nLuX;gl%(pTN5 z`FEJjfsmpiQm|8^DqJ1S(tJ`}#f#nOrrf9PvN5H92Q0wA(tojWpUeAY1TmJl(_Z21 z=}+6PkJb5eCs8z>a%!Ka=&fLA?(qS8j%cpIgNjRl(1)0JDUY=z(J;+?`*gd;*91>z$^Ia6hbOOLJFdn&~zptR33O_)Q zf9K-V6w@tU2(vrRS0VPr#?q$4%5N9QYTtTI!! zHb%E$kvRyWr-a?J9q8BNqC%N&?vM#0L&Fe{qe|a+()4}ICiQ!)yQitpa3nMtSHCGY zzmDuOXpi4FD;@U59v$^0vCc+(Bn%qI1Z!l!ywg-)X>)a8Zi@y^08oSHb?g1T+=NyU zw77Cw*Qs*csNaJwo1k0N%tq#GYJb)qD3UnYIiJqn89m&OHJNeJPPQ%3jonS_LYOkg zevT;p<2=QwbSuEK%qrbaylQXSPAOWn$?i3(VpI)agL;z@YdSZXVv*ZV7*)iW7>Scf zo@vJ`I9u479iQ~CoeqKYc^U+%9k8ASo06=CbjnP zqrya3v4FZ)h>BsH^5CXuhoLNZJ14={Y~$QN7*xoObY?f>EuLTI0ickFwBuJBFe7va z?dp1fjAbi|?VKn`bv_}I(SN^C4&!@4a$z9-w@%O&Xxbk=*p652x(_(moYqP+1Utm=Migj@42n#o!KW19Ghqwz)Le zadb>7SaSs(F7Y0xZ4@&PYT;S9yk<43I?)-Y>tjDZmGdpw7tmRF|1$(@gRy70Vbb%X zO`_PcL%>%PK^#nWY$JGG3(*EUbz=>;X=_$nYM>hzTguj*J0W_IIAxY7iUdE5=}|-u zWtcXdJlE8x6c+d;UR04z)j&|*n;vNT)N6CUDu%{ug-b*RBQoxZAxBW_8M@CEy2)4WxQ%Q# zS6RMocxKdis3YiZFJ~*|T@N9qqD3CaiDNR<+V=ru@~xC2IshPtAKAcjw;$>j)7LK-c;JwQHlB5gy!- z)-3Q=8&Ke*r*eDW0$a=^?+*11a0-4sZ87npp$;8^PF{4Jtej@vwxKESY`i>bwl1aD z4UmyylOcL$fwscoa-%E2vzCJ6U~8dDlX_fVX28Z7jq&U5r{2mD-`fYKL<(lhnTh4N=KsF_1f zdvC3Iy<6E`6z8pU^LWlSNt3R8SOz7f8SEIUeM|afL-qOKdwzP2U z@D)M5RJf<%e>L+=&usaTOs6J44wH>*lHA}ZIfj7SCH!Nm*F*Mm2hI}PM|lra6gM$D z&E)&El`*7MdliD$dk2RP4q+R%!75{KX2zuW3P;%4XCZ;KDPQJ-KD?3f?W*2BBdR(r zK$;S@=gX>cs@TV@tW!AQ?BK!FeoO?YKjcv7NCNYFbT>+D!r63iw}pG!B9c-(%axj{ zjt<40|mcF%jqw&RiyLkb#?vKYh5sKLRenC#mLH_0>r~$+)PY!v(KfJKj z`ECt*qfFP54lnAmTRaaO$o39Bq0te31Eee>6bVopvZ*T%7VFTedXZGcK?ohzT8enc zaIMi=$k@BBTgyb;hXm{ZX{ZhG;c3zEYRwk&0TpQg`P=)t*0Clq^STMD1j`$pA>qnj ztD`bjYrQw0tlCa(udqy6V7EifR(3nSjmip*BDe={)i46BkqhANTbu=T))()dD4WLu zeOK}Q%kyzYth0On-0`t$u4ZpY-xjE6Lv(xjx z6;~Q0!;opW``1zO4HX(-sYc&V3Y}kI79XQb3@`V1?9Dbf8Doak<%gB%vX4|;%@u;$ z2+IsA$>r7?6?TwebAM3ghR@9y;ljP-*9cx7P#rWZB@f!RR6q3x7>c;DdX}_osv>$R zd>M{;t9unpo5Dd(QRqXBjF?CT2%M}nqT{(X>_INr7YCp=*{)#FkLLl;FuJ5#pnH?6 zIMumO|L4qy7Fx>t4uhy<;UCCV9iFard#oI&8$bn#@O;dzN-oNls@p=mcPcm$`URL+ zYbv2f#%-ijfjulOCcleZ^v3rk*2rU#@U7+jD+1QUUJGwH!^z;oZV_M5LG3O+w2aCk zm44WwT3i%RTOrKir?5e5I&-g~Xdk&6Vzf>5(4LBHQu$@<_PW)NzYGP_UAg$_>&4I_ zRPYMwloCwnl2wrZ67yM7`35dM6w4{_1@$_}{3Zm|)6Nxt%0ksHfeEjY!#?@owkjZ0 zIzHQb1Kb(L-UzT$O}1?TRbe4#VzJ09kf!)n0KdQiHiEew1vCZWnNPU4g7~cCo`Xys zC1}R>lzVc@pP~Mwt@$>ZL1CtHe;=4`SUOnh%>Wvh_k%~m&@&22_GV=-6(&9XoEShp zrxV1(&*Ed)V5OqShdhce;tE(%p!hJ(dw%>tXVJFz%r$xh5v0A_T*%#gSK|TgSlwce zaYgM5#fSeKY;Ck8bj9GLuc9CYxLkqX{OPBz)PFiJt#rZ0p2;7eUSv<)P=uPH`p~~$ z+;3yU`Z7NY#}Y&@uk*pw-h@&NSa#U_<(=gEdd_ZPDj=}Pk?hhBEvt6Ed8GY^RbS^z zFsl-GvIIRqWvQodoBIkK0i)HNLUTTDX?)93lBK)~X##Khcpz+t*1W;WRhM>sQ^YfPAmvdbBEZ;TY22;D zTb*n==-YyA->aq1R)F$(-=*oM6tz=%_;k1OX^}nY54?!){;kZwDqo85@<^65%lgy0X!hm#?q?)Q@FwtXgq_R3fZ`T?<%*m!c_-{PQbnY31 zBTkoQL#`1oTes+fG~p=#7x87+Et%RGw2<%m4MGc1&rckS6YC^}y&SP`k6-|*{o^|# zhL74xUX68Vt7e9@v zkAX?qe;dZ>BsX?b(nh0^7SAaem0bO5-XY_IC=);zXUxG@^t>`)h^p-;r7gIb-7nQ% z=C6z$(vE2TlHVqax#T8EeN_*=mG<}v^}CCxKs|d<_((0r+rIC60L>g-$YT(4G)`ws zG|Jn*Z4<9}{knQ<1#qGF?dovRIO+m+_3YE;C==e02t-;v8w0A=FTX!$0&`lv%hYAq zBKM?5L^oQOj`p=x%C>g8hdVOg*47M?RXJ@6qRDg1)Aw{)@XyYCRuMJj%krpe{-oqB z`SoSBCN?CW0slUMDDmvBJ%LDpOMhfX(v7_0Zv&(X$3aUyRu9q>4(1!2QPvf^7F;hB zg379PqAWF-K0-h}EX)%dX;vn(C_9RvmXjhj|B_%RPLYaaA$u{{x%G;5h_;vYz zVmzYv3-sORZn^pAx==#d2s2Z;A0R|W(2PBkkq$Q-gTe`cXySr1mTSV!W zZX`q+M7lwmA%sDO_=fv=@B4Y|yL@F%|CIA0zzP#%#gLpl_0qGO zW=w8xCMNvc`y0}eaIcQ5z4R-k*O)A|$_DS{4FQ$O{4dgG8VeP4G`)7m>g_)^#fE>0 zdTows%u~~@A@sq!pfl+NOCpO)p2_zLnRkYrDi)NzwG=q1G+Xcqr{zQE?rOc3`|;ny z4FJ$$7CH?C_>ccI*+7n~HF$HNr`L+5%xg*q7r(jT#EFdbLcOrMjmG>$%s1ILR`H73AK zrIR%LqJF%O)6rRP^%sp#YRqRvjKk3v4NW}*FmEu4;BkAWXK32gsuu9IrPGI_mkMxO z&3l_z46j;UqP$_pq;l&`F*lQUjsys8X-vn`(Y;LuP|6Uo<1=qNtrVMQ9qGR!MaT$F z>xcDmV&<{it_}HHwHk0HhZUFa)jmfBq@?PzRsw-2q~>Z4 zVi2Q$4g)=oEj0;XVh}0TtZLR&!&My1h_NLmDp#9kUypi6Ou5iD9J9lt2PV7066yem zIh~~M?)My3U|fn1?J|B#A$$*)2zEy9_z(+K$C7d9Hfyh)r9GW%77@hs(~~RQC0la< zn0#M<@~UMHHwk8|X!OC|CQoF>wjCR+SHGdm7ZSsN4}n6K3T8oU2$p>r)>rzv>= z0wp$)L_sM%{&O?+#v=~gb$l-Z)Qh7x9kpFOLImEnz8#l#{HUvkg{&D=lE)eE_$Fcn zp|7OYY-#`O#-!IY`rPLjzCTA$9$I?kvIzo1Wr}k7K17d%N)7+9sf9aLd*~@#QeNVP zevSa7igwLF)mZPQTlApKNXGvYKblDL3#+miYvk(02w=a7%P3}H7;b4VxbKHPQ1$J`!fS* zEdWgHw##~l9`Lr2=q{1qMXzBq7$iA(s?;5G2-GXu+0gnhsxD5?!=u2^r6iijOToLB zfOh=ecYrA{Ac;@cyWx!;eAkiPK9(GC6QkSm#Z_P9Oz4}0H35ECv`%^}5SV#<&;AuB zdLuwd0hsclvf@`2W}42fZ6fnN{^swPbG-Zp3Oz8Y;P0cm!A-J`Fh?(?uGcn*GUkiI znT(Sgn*Jc=N8>^Q@p#vqpV`V)N?O){bFDuylXB;{cx`!Qn>4oEmcmZok)_q|eQY+i z_-#JhEzjH!{|Ho>?u0-o;Kn(<<4t8!I+H! zyK9h{sscX8b5#~vbk8%s=+S02yr2P$DQ;2s5`v1P_@#p{v3f3YRLW0UVn8@#A5#S` zS4a(I{@H$C|D}b@3|4WG<*)S}^2JtG-edzFup-Xe1{d9y2c~@c{^(Bn+4KR{W_?~E zR3+1J*4+N(`Js*URsNTQ)=oSWQ7*nuDz2Zo9!_)aM#pz-38C;$telk|9r>Ns7dH)I z6T6je>+1%_U0yZ1S*lTq@((elJAIq67S`tXzG&wk>C4smP(5zgv#vorNK&)Hp!k$c27$& z)x+u|F3xN4v0Q(sA#?ge9y0nMezPgjm&d6*4oG%PaVw7SlD+hG?wX*z{CE{MI(V3uwhf0zYA-V9rV3@VG3rt0K0HL^k4U!Nw zAzX-3WNw8o6~~eJM1Ig!p674+1-5+N^?zy&NI)Z?l4N$|{$}PsnBphLR=;en!N_bj z82omfK75M{+}~)<83hX-lo~J@eH>GVPrAqu zC?(*gB37ygtE1X_`tth2f~f&70#x7up6ftLdLGht0EWt&PC*N_T<;HrIs=C)8k`n! zjnYay*VdO=j*LdM%zO0mbxU0~F_sg}+c=d+NW?o;rXe0C+NDe?t2B!T>Yz$>;H$!_MdLHc@E1Ke)uc0_P@jv_+vukUSz_|zfEMLPJJ60Y=Owp7)za#o zr|c8sr zCg)=p;~LXQrV3m_Z)}N)o%}I8Y$nKwTlu*iE1iOFveFdjrZnHhGkI0%K|^_Lbxyol zA3Uu)!*XH7Y0gpa4pdH=9^ll2-gkjwUN546g-tV7*enj*IwaVE`k?ln)P%-t+{<@! z6)&!eH4(zK-7aXs1#J0-ywnHVpHCXL?SG}XDsIq-1ZE^W-&wbDCDan2UDaE7;lch# zC=p4T=%zsd(9x1gWX;skd*9?K=H-Xnc3-eb3g7|&V3xF6 zsVUy50BxL2C{DIU-a1QVMAw!t^%jEv(3jOW)|-y=0@=BEaR8Ga2K4K+0d1MzcdpVe z7M0t7zoQPFai_U<6-~b(rR=n6-7)%kvQ(M}t$;^;qSr+nurwI65koSRr4}+LX675S zZC#yGd^!qg#+2{1_V|77o~+Q<^55R}NM(tNGT`2v9JulvCNYn%9@}BqUw0x$A}Gp& z%?km%i$1U~c;m636bj^0(rDi>qfAl#_&g$H>l&nW9vnE7S5x`->l6~C3DjaX?BBAi zzR&pSfg9j>t~n|)d(b|6TBnHf8bp|t;1)!d*I%TD+{*{(9T*^tb3!U{f)h%xhM`m} z@6LjC<)8QiStq|H?yWO)6mPjPbP@q<%Gbh&Ay7Q{MhZ||UPJd;Gg&RtS6kn@L-2@% z8&Z-#P5Rdu;58&+qN@yAzC@N&+ZlI36s+3kWoo=uuLA$nzE-}@1f1Jf?igeZs75R4 zg9mCSi?RQ@ zmCv1|GHsjRQvjnqujvVU-$(en5mKB~6k%s=z>lZm*X4L6c+l{DU?D$v!2=8d&=4H- z8Rg?@4(qPr0fLuXTHo}<0UqSQ3vhDP%-=B1nHuY^60qqF*k>?dh(OB5=^;h%|M0Fu zT#`Wn?XPT1G8Fz5j%jDMhtl9A=p-~!DpV=&#qPQ1_mF@RGKM)Jo1eCuJF2&utVV=| zYyG^;4J~P6ljdVb1h1H-*=q{}#<2PkJF&)T-tfFQJ)w%fPYjZ~eZ+@IeS1@CnaHlB zSHS9e{?pI<&#*1ayX09^+)b%K>`0W=kl@Q(eBV^ypNuyMjp2u*IAu?gO^;hn&3`-# zzDNBO1oQ7ay3P4d6Dvu4BN9)Uh&x_ZNmgG9AVA&Aq+e>rWp|E09~=I03R;tz06aG> zA=mkIAyA66pkWp(tEfQ2crl;?*4oAa8pAbu>b9+>)Rv``yNdz!BiRN_fkUz!oTGRC zf*cqo^zTW*2c=Juz1kD|-T0_8aOIAxxSTO9JTM{i+VtE8U{ad5c%Krg;YwMrkJ1ZD ztuC8l#+)Pz_>zegr3qoC*w}fSO!H=kx2IA&AjBF@l-a^u;~N@nQ_|9xcj*EQE5bnZ z4-~!K@HJsMWqOB`5p6zCvW*GBK;J?4kB>g zN)x*w6UUv1z2{Z?g2!{NSG1oP>^{CO{&MqxK)B|+sg6`2L@*rE%=cydN_s*1 zuHjv#$^^#{GQ#udbr=1Mp@W-@+}RxN<+&)7tRm*TlJOKbccXc@0_ZxKdMflj{9_lL zvsVXQ`i&BxkZtL-v|}#&uvPr0LoTG`<9FZW{dn)d`W=7Q2HuKB_X}Zl+yys!Q%_r! z{G*-o;oR}EA91j>RuN4I4OOeb&wBuXIRnyVsSU7CUTXPSrK9#Kb3wUKlh z02dTEfrplmh>WqXHOfj}T+m~7fGa<@cm7&TlAb>ZK3WF5Gzwz+-CH|;b7FZI{zJUp zRZ`TM9i&7dZ0zbAVsBa#7&qB7xnxzZm?9L2_eXr*UP(nwWk0(S*rSo-iZL@|{)_F= z-EQLB7_4c}{^lX)_j@DEDqwbq`}G0OX@(fvxaDxdw$k zm5#xbh0@vIf!D*|8u?;ku5=Vl~urmQa_5K!5Nb&6kU3QN0ULDB*+*>U(v)|=a%2EkEvV5Rr zPV!jyZHU}83S{ml%0wOOFN%YU&AFwGuVzG|zR}?D>gL+aT)6YS_gwf&3}=0Ry{Pn< z^uv51u91$+t~cog{|_PoPRCv%#yC5becUscIeQZZ3A@kAf9t|`$ov}Niu#ZZG|_Wt zP4~dKw_;sUTHPr?FEPia)i2w zZx_*wusM;>`N9>0ns~@l;#+C}cEmM-CUyM0^RJ+vuSae?p26J_4duHf*{PFLwBC_O zMU>MxqR%PlAsb%7&P3aACo5TlL-#|?r*wb0-ksXPJetoNi(WH|Ga*mrC@MILzE zAXzzh!cpn2QOZ~j?8Hj+=q^ut-bC+dvZ9W)cy5+)2VaO4f~=hUwyg>-!7)4luB<6U zxLa_KAI+LM(5sk0?-1h!>>-zd|2Tae>yl$>N0_BWYc|r}vKg;g3m`77NH{E^XqlI3 z%l0Ob?~%LOi#!oq$-K!e5iO3gybC2!2~WVDU56cC;Ju;{&kwEn7!@@}t_7QXzFIs{ zXK9UJybHOn7q)e>xhCbZhT$I^%m8D-^*?_n^4;>p8)97+_<7s0^TwTnUlbFq2eIk5;KpY9JAB zIy|?HW}#vd2v{+nSO^5dGgd0Y0W<$jPMrBm!$bSDiRXR-?-3CE52KlC`Bd(^PGNp> z7y4URuy`@=jil|mT=QSWpghGNQaYCDEBB2k%dx<;FoVo5;OJe%}HVsww-K9Nr%Q#cd~m-@G*lihCbv+!C4ft+jT+Yw>KVuj_Q% z#k|u|Uh+C^2u5eud`jO@#VGE+t;BC-(u)e^uF0%2un?BzAbA_3Y`N4@aICj=pDCmT zcuR#lNBE*y!uSbK{q7X%1*~C%OLhaHUo-^O{JaIV^e;^^UQhBdm+ILg6{*66Uyme*s+w^akEPkV=fv*1tta zwS&f*&+@wsjZGUt75KsSt1X?o8xCGRi&vfWe5Ak)W^<{+e^B_}bLyV~CHf>z9wz^< zp3?vJ2v~9NuSXwYr;8SfZb6u?3FV?jH~pOxHt}Z%Acca*i|cXQ9V0DwmzEPKQIXz= zE_>RLEzpuPzr&V9mMvD8whmG?x17ET~Sgy~&D-u%ZTSBtZ4-m&WhYXka#35>JGN_v3YXe+Y28GgrmO#y6{CfKEC# zi>H0}22MoYf(gQm=7N7o3^@67JKw(GRWc$@MVm}`*1!@w0~{36l-6p)o!Iy%5-*1| zj>_d8fWP(ahQ_42=x#i0d&W;BU5ZlVYFiMe5JJ6Qyn%nlNdFep>de2BjlimY+Mk+J zoO=zw{mEy+m`4)>dj`+jhYz!!CAq6kc6QqJs9)u|>y3-3`gnBw!F zR8~EROR|WW3$Eh2dmv%4Qf2W)O2>VV9I^%fYiF3||+o#tSOy9PJK|!E0 zGoh<7af2@mJ36LKt_(P9%-ezU4Xe;H2lxos-gO4tIKeeBp1GGyHWE1RpGlxM9GlWQ zM9v*D7rAspIa_oN0-JSQCw9#pjz~fa@kycMx^-p4Ks`EJtkTsSo?1f~2eoIOrS*C> zdsY?cD@mGbwV1PUIJK)wq)~(1YYx|g8>dOe8|XgSdRPg)G{#FUvfSjp0gUljXF^6@ zyHfLsQOM#-0ssKAn4D=u^WKd;4^M*nM!h)^?3jtRMZe{vCM!)3Si%QYoMs&5GVs?a3kDa%)E)Qwu zXY7bmOeW0J(fS@Gz-dv2&}5^d%Dq9q_wP09Ilb)^4d3xu*3)N}%FeqQt+zZ_O-*tZ zo=SA-wPk_U=>n~%QTp#A4Kg1IlG|iwMDW&V(bN<_%jnZ)l8U<#whi~W4V`j)Q_47D zogjT%o<$JycCU~5JO;`#{$U*;9)mnxl`?vX+oMgl7KTFNIPQ}AM?}Hf#c>}7K`wEM zc^o;=S%W8qO4p&T>&J)Q7#n8rsZK=cmQ0l`sP)IR9TSIO@ar>dR{n|aV+iTbK0sE} zK;gK(BHxIz;EXp`T6?Y&U96wDx!Y|1&0B z6v%(W;6d@9{zlLXtmD#OAJ@xHhl`Hm7y>fF0@cdb*bXTe8Mv;UPmHDmbDpx(_c-rU z7G81Rzl+1VInOZcZYGzs>wbJAX{J5aTDKL%!n`Nt=YV?@&%IQO}MTrectcSt)BeLU*UTG@2KnMB@0R%IW5dPBPC$>t+d&T)G{cQAI@p_}s zHnu@$R1~-E@U-Z$hyBmc?>3;DuQzD1jReDJ4C@etP_r&|{kX(^5v1DSqi6+OCyLk+Ma36oDf4SQzOIbQG26 zODoENrC$OO%tEy1DUKcE6;3>1BZag897tox&<2L6S*O=G{wR0imctJIBN)vtK@)Cp zH$w2JRGy71PX*k9VgaD{zPO{2Cnzmbo`>hkI(8a0q)OOSlBD-#M_K8WSsXN%(VCxA zz1*w7YDW4x=)}ys{dHJ@LxmJgMsfH6K0cR)W`_S83j%m*E-~?Z9!0AY}R%Tyfn(s1Grx?#=LJmL?+k34|ZzJ}G0 zzA;P@(OIo6LWus~zpP&3(hqmSSZ(^7_hkw##+zZfS8GVXb%K6fB=*7~T@#t45I=%4 z+hsecI}UJ{;V~v<;36Ym%!Z~mh`1XfDeJ|t>C+G}D4F-fyQA&tB{Y7pGuaJay6y0a zIuDHku$hGV0-b3PJ9*J#+V%a)t)ro54_!L0g9X|JRIpAhla~-N=KUy((iDJYP|X*6 z?P%IxETlsRAIJMX=UT$I+Gce`mo_Qll$#HbYDL-fixx$iGv#|4xapp_7xGKe8S)eX z6iF-+Hn$E$}pX>e}2N4t>Ij)faFEOIE;N2@-jj6)(0gu2<4z_32b;lQA z-C>P=m|w#gLLzh^-QYC$^{Z|Wyl_1aPE3#MJb4JKozQycKwQSZpYMMuHY~XDQypsc zZ@+MS-h|2iU7~p$l2xEUA9@ms9Rx&CbkmdF5b}~NEc=_r{l5T`7|zvIqK5zfM1_B$ zOy}(xT2$&_ir+Dz&UiRohydf%I}P7U{o196T{Z=h%{QU1h39iPq1gBR^gn**ntRDz zymxl_l6L`x#pt7lhT(-$4TEDJ$G?%nv%Crgt+t3Szy?O{26Sx73N|W3{cs<(?#tV? z7K9K(i2^a|m)v{tcQx}vhjZUuB)#7YK5klZz8p_oC~Kyl+gVHxLlbMk>$S5~TY9?F zTRGpY^fGlr0Py>IK?*U@jNkECW9m~`4I1#TDk$A{?1ot+`snL)<}@xg1Ic%Jsmgqt zYR@-a@ir0iihfiVt|rjO@Chm-99)TdjpkEL9{<2EfcZg$uA_G23yQnQ+D^|z^f3?{1|7Ec` zl7lzt5bky1M$0ZE*rVeryeI+HD-G9#Fi=G2i-0HTiW@C2j_0dVErau@5ZQaB&-xDj+6#61M$I(*CBV#W5ABU-kY z@1H+^R}=H=@kcJEa?U#5Q&=ifE^k)NR&4h<@UE6dCx=-`lX@m3a-FnsAs`B8Vn9d3 zj5luotA>NDHFNiMe%j@&t5Xf~n&pMAZ!}0nhQ>4arSyQ?SjtD<8RakDv#+8YB$_s@ z(}V{men_&I#u*wwgRDrKZ)mrsQT1$>EVFq9SX$&!|8W5HUXq32D`eK8Z;x@|gnM6u zwV6BKM&DLl)~4?eo!iLE#>G9wID1oB13TB`SFU--q#35((YL@H9b{FRJk#PXm0V+o zzIlLCiCbv<)vN03?T#dn?pP+*V@KhO$=>&FbaXdLNlnT#4%Gr+f3o2E80pV&ZW@w& zRCfX_#J&T|)dS)*PJ3`c@OYG481RR&SVPN?Z1INbgFFg~J((lbn@ z!xC-u&dJv!U&vL9J?kmjXXDRs+*j+x*I^;5?n5j-I&t zlamAiOoA3|Z9$1tD%l_S#WLtj6tF;j0$Mo@ZunVIs(RTSXTuL(yd+}xHj0_|;@j){dEX6Bzv|gp23tMvD>j{1TJh@e zu&&&O8y?j>wHybnUH>F`08YESsNL$k$+7=x=~1lOjCs=sNW1KmnF)|hRR}p#9Chg~ zD6KV+0h23=@2q`wG?lf0cTijbvRdbaXGrW5em9n;4`l3U6KJvzI&Z=7@yv%C%y6`r z)=~RseI{AJGM4?XW_Tz61fb{3S?kcfz(xxVG@UxJJk_X`IPG%|(i8W}ov&S-BO*7U zOv=9nQ>DUg?(QIcFaf}gJOSZI<7B~*@NXt-#4Ra2b3_&Rty-1FwU5fc<$IAkFoVu7 z890uGP5kd$Aa`+;qFbCn#U>4<*KD5ox2}ItiM?*5PgQp{>td@x{|R6!A|diH8@gs8 z^o}GKRSXg5*$~j%p z?L5ik)&@|Eq?_lTZb7lHMXn&68bElctm6Uoi&v}NibGoi$%e=wHbY~`@2v}Naljq2 zqSUb)8_Xjb4vdi5jplFnnt1DgU_8SXLOC1Iye@5KJ}Uckn=s3)&j zt=$LMDe*JnP157QoRv%^)7DjZJKRb$6TpAA!O#==q_PfX)_RZOXf28M!!`1RiI=`w znda9IQ2ZbWO#kYaJe^j`^bjasBDOi8lp_#R4iO(XZ-m}~Mx&g1Too(Yfe2?jL1T6G zMcURD+5Uquv&TPgWwZx7*kmGU|sa6nY4(fvXD z&YP?}F`}gbX3bx2z;94r*NcN&V9!6n=J!5VScMKcB#EFqQ!>|CpXwW=pGd{_9(?i;D}@flRP{A zJJjy&7^7%UIu*z;RaKp}BN2xP9f8V|-;6L_L2+5ANguYFfc`r9{Pn{d8??;2u&l** z6BDp8dJ8nqQ^&0L+Syl>bmN^>#4dlDX%(z~q6W(e$@$=#L(2*JuJJFSVXN2%Gy^!X z%6}SHYgEYR|DBzrAbzr_EYe7Qn8Baa^WWt*Rs!?FsOK+Q3^&$OT+i{=t>HFJ=7;3+ z9|1vZ@LCy+2b4YRTFcKTd0Cd#lof*(4Od$*pKom{iMX=bMAcE(v^ESl0_-m-BN3> zs@H?z0Jp=3#5%E)@k4*yejHcg{;O+_SqzB$m1XJrYu8ZevLkWXWS&c|0pvcFs_tpW zEeU+uUKVFTSCrtXSU8)9d}H6N)c_|>~=4i?Do zgN_4y@<^l>A1{9&J3^WHM4!J+RzCxXf9|m6L6f5}K=Yyi;_pgHoeRH}M0P?p>g+{= z5T8@!Jqy=Yrkq_(1;o0>6&2NriZ{%13d~MA73Lf+=8POWdQCp8O~z^5BW2h4RvhZj z&8dqH;5~-ULrtxw*WV!ocP(W{ zW2of9z(j$X)G939%bXBPcl!`Xs7d26%66&(oyz5Vf4RO|Nqi@iXwWshgn6! z0UBz!s3@+UkPI{H^Akugi_rz{ck!8AkS%v^;LA^KF~=jlaub2wsa5Lq{hxe>O`Af0 zNTDRTOo31nu1VrcutY1|>JX7o&4rBOil!1kRna6)jWnaU(8lCC^mp zFx;jJEjyH7!qnu#5mHg$#qimmk!EcBvz-^E#WEtKod6nMvXX)T&@592-qD9VdlJ{KEO#PQwR!_} z#*6$;2p@w78=xH-=MPx$&r* zR@Mk1_&X|=-_|=Ktm*y?vN&1FBv#Frt6ctm4HpNXM{*7xWx%FaFUp^3ccHvV-Mbo= zM6k5q8Y|pwe@u1nOg&YKxLIX-p;Mq#zchkE;clkM#vL-KF)&(T%>N!|(&pDh(XLhB zeRZ+ttn|_eei&l(^Mkaj4gTPUNk5kVhN(yjYSS*9)|rqm zSdOHO#c(x@$+i3&eWIgC)*+08!=FfOcD=GR9toj^i8RhKMtJy|!TjV)5q-jVvNcXy#-+XQG zBEkX9uu=6S66S{MiV6SL!FMA^^nOXK@g;UJYTK^qBBu@fq6Mj!0?@hMU>G=gtjE~h zBSHo908*c^{0Y5GRW!fqxCo)`EWbcz}t#@9B@KqSHG05et} z=NY-GJHCITMMPhef}e1@9EZ(_(YYR-_f-}xtg7U=lRpW^H;M9pas(YMaHu6%7=ss?_-!#T5d@k_2V zy?0Y+5G-A~w%Q1YPn{g>ks>I&L(1iRgr`R4G4bF*-*98w?cQL^zCqx?%T~kNv2mlp+Fu7QgMj z9Q8D(-TtxN`G6_os87yapg}Bc<#GLi&}h|dX7ESYH0J-(z#p`6;3M=AKg)yuucDYD zYj~Wv*&5XJe+YxWOBVzm!L7d*K4Q~TaLb;#QY>gPazvwX^mz`&8RyBDyKq=kgh0^{ zZ~Pmvs^5_N@0?X-k3=9(9${m)zd+a@ z)sMR~$dY9!+{)-K*|6ic#DO9>%n0Eh1?H$y$HX*o#e!9GnqkNukDqBnVqpgf0vDQg z!AMc|bXAC3bE58gdZG0ssFlbky#s8b%^|O|ADQ4qRyh_4`5qdG3^)O3;1@6Dr=zjOgXq5m_jX@#tg)Zvg!}?H?Bx&SA<=u~$p87Mcddns!1Qpj zPA=)}TqTGMn1u;C;C}b%Ii=gnk8NF|a^*rgg1)EfrjL+^7LFVA2(V11^ul5U3wN%q zJ!fJIdoUPPo}rU{rsM1gvO}4X6BYy4}yU$Xc9Ro^xJ1AnUoP&!(hSzj+zYA{EDkRRi zFQu5az6przWhha7m2bZs-t850H{qx`D!+7e6PbDKt36=1$&}J??8*XQKd2*+au!ug zkoU7I8!7v7bt=p8MXrLVCHhlJ{K(^)BXja}3(Il2*#DJz7+j)e+EG3=iguzCa8p)t z`1Ki@0Ry_kU~ji@m|Vv)^-SWpxN6x~Iu_Hn{J4N@jL!oO(=1lusKrMKbWNP9UCjf5 zpq{TFmAJW5@P_*56Zyj90ih;(G$bu-qtP91Is{KfM3pyd36eezjDs!$2O?We6Aq0x z^*ON5S3(_Vpc}I`LEkO>w7HiRBYw>+d9&NZ4+EB!emKi zg_Kxjj2yJZGycISZB8oq)d~>?&EUmh_M!*FB3y4ms^0Ui+e?8T9el6%)kaxei-$F6 zG4vRF6X&Ffud6mUgN7N`j`vCpx^U=qWi4BM9vjVIa>skrMNzx!Bx*1z0T`*S*WR`C z$HYnBz&Ea72&-I5xH4B%X>HeS6}*Y`ba?Iv-qq?k_}_ZPpI)m;wx{dIf0~SZzdV)L zqyKfWX|txQpzS{~q8sCH0OxyOiLLW*| zKI8Phw7YkMUFXOdp%H=2C5lzDl+VYQk?_T+->Xi1o46!@4xk zqqtM~5i9_hrYdf{P(F_@+5YSYAqClOj;DY|Regl;s#={Qo@*g!&Cv>G2W1S@b)+V! zRF^K?F#%RFgJsUHxa4Wl8@#A+>lo#k_GQte{W!kQ#pxosEDbL`8?wI_G#s$hS{o?q zCO|M68xWnd+&Ke%Du8 z$N{l9A~BNUZG>ovuSxf2eu%o;6frcNX^O3>DC|@L*9<}IK%k|#&Ui?wP5q}_$A*eV zt2N5>Oh+yZes1Ayz&Tgt2FT1MV@3*m&Dgj>k}O@{{?eNq-!F2X?>G$k*V>@p;DgqL z`Uh7Ilan~VQ{7PxWtW7EdliLZ(OKY?bk#9Ql>8lCk@4Qi)=^_Z`m$!g<2&*8)>p0( zwLGLzat%{rE8WX^e10IW<@a)wx#{3Z{dYvYhYawDs>G=0Iza-{@ioaxrRPG#TQaaD zjGnee3lZX$mlix95mOO9olE=Tw*1?^8_M29Krx|Un}g1^2%@C1Ju%Ow{$;}DHgmI` zoq~ialA~ic+XfnFIwM%T%aco7k3)x{VQIExw>>=yGF2pum5?}U+~PA%7Ie245M*)D z!+`XsuND&2$0p34&JtkwPs-$Fh&`xE*wcqpH0j;)cP4Bjz2!9)j`L<~SBL7(&kX6Z zux({uDslK_P(boRn8Kmcyf+5iTlV@rnk{kPJKlwTCLG*K3gx4YV(~hA)4cRA^VgVc z)!-!S@fFuiNywm$SETsbaZKl2M*OpLgUSv6vWS2@i|8H&0QQ8y#QW;5>EB`XFSqkv zdn_M~g*Mt3J5@D@y9?3K+R~zL4ZM~eHEYPAfwi>;ld0#xzgm7dPs_Tc}T%Lx!A()qAz>mylWHBeqt5hhgnsh z{uW=dt)@>_H9T^=kkmr#>LBPtKtf^n@%2Nzky`VNXn<%)ontyAb@xF_H1BtXpZ6Hf zJ5_s%Ft4VJB+k(&$L%CbI-6D*k$@)(SqmMBydjDA=Ew91;-xZ{3k$;|g`3)0$bNiO zGa5%TC+!*wJ3>uT{)(2ArIpQ}CPVJG>y^5hyDuB#RUKxN7ZTZoG$FN+d~VYM`CG^p z-=~Deufu&oPFc-6A4!8oi>i(^(LzbVRNXx-;scyxp;b(l?9E(H>~@OY1Wz;QxkW)#gk%?kA6d0lTMu}={7+Wi;K64bzUr#-zu%OniyTN# zG-Y5_D%q!h4KtvA7wZ24g8%I?xKf+UgA3OPS0V&JxT`o@u3gzN0I#6Fj|dBL0W;zM z)>uMR2R)d*Z1d4!5L}L%wtec!)vE}I{_WQIR**7INp3+GQxw$|c0i>$WUh)R6AW69 zm262uP<*${f6^-OoI=O{q~9O#=aMBftQX!2J+71JTy zCXmKcD_&HHSCkdBRMc|~82FsSSY;qL*D#3?Ax=A2H5WCDVhs-grPou?VD+y8!^z_9^ie;=Ko(NAgJI@IxP!q+xlb1eFyJD5kJ$3C!H5b8YSX9)#K4(A z2)!JvE{ufFxlT*1!$Hd`JrXcUBnr@fNJ)mmDvIh@$V!J;xiK>hh<(KnN?XC0R_uIL zw8+CA?)FPC6-A`UaSm!s2pPZe625H2WfB;kR=ztm+$~c&kz_O-4qzNOk;5PR>hVci zj8s0DR~-eF+|cldg&3Xx7iWxI2h-;`#R>BxinPni!+;F+=X-M+QbK##WzS?g&>I}0 zTz_hK(sk6}X6Rq>%VOCaM-{X%5%ur~&SBB0i6Q_7~ zy`?zU6S_&ia?@UZptqc0`_VG0<*3_MqvP9_4i1L}zLL-%tKsQSenq9WlgY>bY^rw`o=awqhQcYa~DL0Xq zx|>%NfY!;eD?)m;Bn>>@tCTcZO(0!3&mQ`z%8OJ-xq+D#Q_^Pz#&8GDk5-{d9J7zL z%qjUwAEuV;AGL82V4MRy7Hcl2Z@*PSvzu-^xdI8Vx`a=o;GLovUbjDT753zf*HW!? zmTAA-I+v9ijBmZ#9fS2l`le=R26q^zJi(`Zin3v*Ss#oZGcP~X>O-|1+s z7fQ$~dQWhM=rDRJ+lh0Q`;U#h!c=yaG$G02LNMfsUrI9tJ95IRks9c3Y^i@HlHUr^ z@*EY|Curp&L+iNECfdgh3;60GDV6T4#Of#2H|-kzyFI#fll7&0X-n1}i4q3c1g@`% z)$Vrsowc@DR|BA0bG{CkfKD|fD%Gdm^o(kAW^{wxUIa;j5N9(t2P%?p%{Qo?Qln&` zay~d$;(MDTUuR`ZvyF0XLorqV*>ES^36A99V(0L~+lmE5u`k9mK>O6=*_*|0npCkH zy`4v{YHfc)W|983N?k+MZg-DX6+_3&5mgP;8Zw0ZqH$&4xdq|rx0QQfW6qB}ZeT5Jn z)G*>VMuRC3NoLDVzMJr^32^e3O*X97#)^`#2eR2Kr9OK%2Te?VmcT*+-bb8eeb83v z{XPUQ6I{m7`_v>lR0@c}H$(_Y;5jkF`xyS|6OA~$H%f0k3W;B0U7&De6nN$o+pFvs z6w&=yko6WFP&L*E3wndMaM=ty2$u+}fZS(=S>!4>l0@UwhknEaB*urn7MyK#d971wz#_HZB2k|@*y-SO)?CeE5if02dM znJ$Sf@&0|U6M_Uh{OTTe)SviAUubN%W=$^ZZlx7a8h{%Tg zsEe)`qsOB2yrDk!?>(aReBHr(z$B37frn|K`Bkct<=tVrO!u>q8#e=|vq84Vlj|HL z?DZeG?Av>d@Irj#lJ1(nU-gi9(4E!p_Z8-smdMNF-%m%eFk5o%AUyl+UY1k0c&_- zuLN+M1Mh?r6f(l?D-U7A;F;P#j~%qw=tz(wK<~Zqi`~P)C)!VVnyA0X9U0It$>OF~ z{Jp)yGg8_S6VEVazp7JRP$GW(5~UKQF7|Xy*WX~qY2ZZH;gV|9Gcj(DGG@l#xbE4e zX2uCw2GCk;L27Hqv>NF0VV$NHx3{N^j4w<=B1pk3?&uqYZVf8H=pc>JKZumDP2a?M zP(Z33vt|)ouL7K=FCpJeuF!R}hSYByl;7H@+tO;~eQs%hAV;s+@C`zg=Y+e-P(Dcj z&MJQO9K0Q6HCkPKb8Sw$7_@r38;;0_MAVUq*rycCPIC<~Qvs?s)#~9KlZp)p%YE3% z`KdC)cZx;67hnavoc5TfAJnXFc}Vo?{8sghdv`T$y~km(M%F&-(>GYe{amf)5~>w2 zZpuTZVL@X#-14zfv?PhE1M;5c{+nBvW_fbZ}Zvk^NBmf7Cx0wER`3>Ur?H7j8`p zAtZXr>mlkp3;jE2^$*4JulegPv2kdgXcQw_HAL|}&U0-S(v+N>>ELgh*YaG|g538U z-!O(-?(GRkQ8zLCQfau2O>Q~n&>a}>+*A-6pwW+ocsKUNkSBgEEuDf_v|_mK2L$L>7WWG_AdhZB zM^K83vZf5BlZY5NFBpZr&rXYg%8ST*{iV8E03qOxn&UP$%xzEj15i{8qhHDEzQA|>_z>U#FtYyFOAzZ?$U%{}*h#pgUv)Wu4yjYhMaY=;l^(r-Un z@U@BtP9i~WxNh$dz^1-wIj+UAy*Aa-g_Ff;Iuzyc&U^ebwerogc*`q?X+WJxT6zab zq#nV&2=4i$Yd2(KfPUsn3o@_-5cTSEV2K2Qz%OVphSz^QS&AO?I?Yrq4{k%e-W0nl z(Iq~qBn|}TCP+C;Fy<(DnO4d(HH|=6k)0uZ)9m}@0(th>aT+>!Se)Cd`J_&nj@8ezSYVbNLMrwGAFEjV|j#^9x>|6VOr;C}W+|@?Jt1xpc<7ww2 z1vR;uK|U-94VF>&kdHNbFzGF7gaexO@^g6sD5MZ5hmN>;dkNA`Bksp;^v+qRu$c7w-| z0|TgOY2=bkPNJcr!mt*R=M1AQ8}l5sc0FU}M<0h#iaxi=l46&FhSf?7OUdOoeVE*d zS&D+}*EM4jYsScOWlaO*??SLPsOYNTzB$E47}%VgJ*cB;G1k|#Fq{!lf)Xw)V|*kt zO&3o3KULDf5QSAPCH(-TDTLto zT^i`z&RhLF7jR%=SC!k)jZ~mDSLUrkqo=chaPnj3kgG~T9mlwB@F49n(aaT@*ok%f{MX~u%??O~?Vt>;}yRqGAXm)m} z8Ms~##u|4+z|4B{V$4JBolYBE)u!{R>r!?t5?|oYP9B^X7z41>aPI~Ad4yK?t(63q zVQH{`zdXHO@3zs`LDI-F-3#O+E+LOK$(s?b&bEuCy-x9CA~V+Ee^t>5(4##>(YqzYKvsau{Z!#iO-p9wvH4~n*<#TX%O3?TB2RO9Vb(1)Nzd) zESOR=TZbTeAJKeNm+6t75syezxZYatU$in6PkR?)e#%a9LyDoy05eC;7xhM_Q9cZs zY_Wc%NOuGRs?m@c@)~SKU0f{Gbqa2D=UYlB#C@?rziLZL%ni(gl!!$bJLrD0$Rfr) zoL8*7op|NPYje3_6gF&I35?ljp-;OhL{vhsar_aF(QgM!ZJC(83SqmG=aXQ*2EZ&C z6(WeiZp0;Rg_;cH7etjsyR)j+VZua!981kxBN7&iZyxoxmvxfXg)zU`mmT#Sdo`cV zfP|=oKyf;>E^Z(l^M#Y4{4E8&%J0cz6uruqLD%{XcTxGg`HUEdH}+^?W1N>SAc$~G z=+~Fy3fL2Zn#R;Na~;c}-^-?{V1;X!EC0;GbhT2Bmx-CNth;UTDp*rp@hB0Cj^XuE@Dd@x&0aJll8FIve%LebpVQ45d#zDx37l z;FFQ13hbja=joNh;_nk!1Skl0t*KoUrp4gLV@RFYkwS40%UJ4Poinj58sLYmM37kZxkKW+65Ucn{nHFI>50KcVlYqgWy&`|86ECJg7_<;as_~rU)_ZK} zq@Sy2-kNSEcn;K4x=CKk7~Xj0u>KBuucjr!CtDIx>S2?}uK*bL*tqZ=bA4Y-90m*u z%Y&YCH7=oBbiN4@6*1DQm0d2>sycR|-@p3%$xk8hUn?_6&6fUz!vLBL8`d%;qfeAa z`}AXlu+gUqulruN;!gB!l)Nay9hCn7k77MWap}eLa?*4A7s}8dqk2A1jyb0SV1l7| zX0yCU&7QHFVM5`M7ikV-q`yoT-)RHs8{o5r23H7qHS$UF)Z%w77!XUGncoULM$aJWW*g7tvT{%TFk{DdN)Tiu z;esiz2J>0VPcfitKrNJiIi`O#4wE-^vqH$e_gS8b#ydI?k3MJPH0Ex!!{3A7a*V6` z*n0gbR>vQA{r3cxUwlCWF8O7xknHM{D8L#Vle!mNwFZ@6|7uuWvs+-jDkX|HjPlLb!)t%HSfKc}1BVA^91V7T2Gr}DY?w-r-56nKRyAnLv9Z7om3LWE zk{T%oJhg+S6*oV|HC1G=_~B}STtPu=y70-GNnWgz^n=%!IPU%s-vqbiIdh)njjiQS z8E&@W+vygEfXTj|Q*8V{i`^o8KlNlYr^9Y5R9Z?)mEef9d6n6R67yE_tdbR}IowNA zU31US8DLp#?ZUM{)&1p@)kyl=r6*uJ-IS^EUOVC4iYdAdN)62>@=oe=xkW5gyN#A7 zD{Zo|3WM=2v^h^iN$}i2wJ7k)VH7B7e0vw|`vp)$)wbWfh}}Jf%bXv!6v@78tzmot zJ?lO~hT`itIN7q<6g;jJG{YBtY>X(2%q(^m`ZWJFs?q1bseev3>U^Z(h;I(4DR2I7 zP09ABrZg}3$^WOO6!=|JO02)II4Rp1XIVuazEphs@LgX z_Zbii!vY{DlM$?@RNCM3EIMR+OcnbHwX+oL(8NkamI-lO;fv`5D2H9hhfL6AhHUT% zXadk*SZ}h?b+fme{9*|Gk01C8{cu==Fw5k>&NA2EZJriDgt#jra z&k@q$7DYqb5YXEL@EMrQ+t6pjt{8G}ENcWv)`JvTqvgYuHs62G84LSWgiwGBZYuF0 z|Mnvkkk##5P)asnb))H6kGfPIzkW!pCjM-Vj8Y8Ecn=EYc`?jBQM&x#%+`_ zmH5S_|F1K4XK^(*xBorvwP#A~ON9ssYSFy3Zqyg1)e-8rmtzUYWe)mae^vSi254(5 zjOIDs>}!wYLlx4hFkWb8+qoT;n$}h>;H@a}Bn)C@LJ#(LQ|Iyrm-9Hmp?D#kXztM? z?PS#X2i7$UDKvk`T=Tk#D|U>%gTigQ?XJrWNLPN5iG~|7{$>+fx9A2~^dROmfJs%s z7YK%K2Dug`-Hda%oL$C;50@Jq-#TL0JtP=j5&9_ONuTMjU3b9oJ#0D5^{tG|TP(#5 zO^s))6wURnVqX(}^CN$Mz9Wnit*)r4{dW=2g@WLpu2qqtTJUYJMTr1rt?t#N{W{!_ zo67M$g0CcF`QyUKW$V;=h1~V3;H&cBdn5X3tn~k|01ixB#I)?_BTKvq@al;rr=8KU zdh+|dcYtlES7xRHIch?(F`f4-8GEBL{&edcrOTlN%ris@7;Z!qgk3yoszfF6vl}V?Gdd-v~GEIQ;Am zSlZZ{U$*vwWz$0m^65DZcTCrG#HS?;Y0^GvbJB9jsocTfu(O1xieEIMQRamDKs>~5 zax@)ZJl8G3#u-lcMZDSwlY0FdS|h+`F3RVeE#Q3EtwWbq0wqm7TVm<$eN`CXIW=^C7ygO}T^9fP{gayzv4@<> zEY&sji~^Ld>!Rd; zJ{p?}Oa5a%|Bv+#kU|;;K$Bz-GO)jH-KBpK!T@_>Gj>|2xa#?OMCH0ztcEn2;%k@Q zHbXPosgdB&Nb3bl9zLw~X+o)#S;zgmN-z!jv&C@GBVw216s8gVp$Zr?DKNt9%nBZ9 zFAggcHR$y9d(^9LSf|s?cKLp0O)p`vI|n8==&ddhZomD*K+ZFC?(w;p+K^G8cSbEvm7wdLnoiLi`lto1Dq5yCMlY>j6h17tbg)s|AQ4VsDrWwogD}peT&t!5B1;(Qe235gGxI1;MKi08; zun)Yna&MyZ@8lzk32z3rOsQ5fA^R{BUQ??m0ozMZ!L*WicX*SZN)&BLcVf_()P`Z0 z^5O{VT=*YLg=zC5lJHrYpwCtXkqRX69teOgL?L;0;N;YtG ztQ+;Zw1j=sT0^QX5!Ln4h6R@I?U_#c`CUwiF@#pAL$EM_ueWtUZ@nMR`RaWFgp zgklLgsZJ~Nz6M_?&Eej~2XcPVKJjLP9mDSq<7zYrZ9UdHo>iqi6Nv$3ucz2i2#vhY zm`Tj}$pki&&dv(-ni1Kl4Tlrj8(5R2X1`L(TVJZiAmeg+s4VA|k?s=8_L?eSFB@s+ zaRv`522~_IOoi<$ihn*1q?(DMqB87qR|g~R8YYL06eF9S#~_kJP9As0b;DjhqfTa$ zjuK!ZufO_8H~%?6Q~PhS z+7Q9=VMU7(L1pWj_p}f^YAL&y+3iI~F42wht7R()P^VfNL-phOVsS>~ItdmwhZV|% zNntf&yJwbY7*B%sOUhp}49=^8J+@Soe)`%kN1$Fa9;~s*HU33K;hWEus@r#wlmo+8 zGs=8A_T6Yl_;b8dX@?&wB~Jo&!7DjCTF(TeKt?HjBGt@Jg+@|^iHqN4K@vh>h4lbb z8whC9AQHrx(Hyi;)a!+aC?4%eWyph`G8Kcp`(YV4!y+agGG|tVe0j zOaksWlm(ny&08}_D$56G9FCTj(+0bejynWd(1EW|3mo|{d<_TkTMNyXYR6PuI;re3 zHGh2-gN&(@0Zfe>j|GE}8|<=H>apSy6=J|MCiX}wat5?ABaCO+Y6@lwJ^-MlGe zvU=BcknhW4UYXRuXrf~weX}-jUP3_Gk%dJ5)f8dllt|pxKUB{n$5Pn2emV zQxN<7@U7IppKRPRK3#Qn+}lZEU>#-mU1K6Vx+v0VCr~$xNdR^Tl8UIx0fv@A)TF$>YtkMgKVh%`b>%^T{gQz%EarU~JYu&B$bZI0 z*G2PLcx0|5YjY4BFHZ8y^-PvPdfj8dCw%EaWAiJH1}faCzNx>_sWmz>5y+iC!Lw#Q z5zK_1F|QiiO_d@_nm^>u9Il3v5ECsCYQ&VtrN)F?6dv&Ad%Q`i`Kom~`&q?De+FXA zbtdqcZpW$uW}F14Cnk)2GL0@p=+*F&q@8~WPE;q*ep5qhXB2P4Jxx`%*JiVJyJmk` zDRxBoMS~6?rLQ6rb+{y##X}!C2~yzl)F1U;Zi!k|@@Gxo zSD`s=TB7Ug3Mb>CUGko6k?yPLRuHO#*)rPztPXlDS8-@I06y+AQ#35l{=$YWqwi(Q zr?sX(*6-7Ko1pg~weaLe+shTV@6|&#`Fg6lGVQKg5aSiY?R$Ae?#MmbupLE;HGiaU z;Ggs!TB2}4lJdBgnme>wA+!MzptBzqqRt=9@hj!R{VdyEXDHsS8e6|O;gCPZVj>*P z@<0O}trgEtlR*_5Q?STFD+JS~T`h$#b@3~a#GPMFUf7rTYHqr*U27aJXj~~v`Hs2C zPv?LiL;xC}9qU^Q_7bMTlwy|Zxk5IfI?b)KR&YX?BWBQ#`o|qHNB6@V<)R)M&5)F% zPEo`Qu0c}N`)Ca`YxS`-Yf`$_Rq2C}H7}9mM#HbvF2zahiDhNPOvINvdDc>2j|1+r zRX3i0-CXD=Tk<;|?>D~vif!Tuy0=!UQfgOh@&6kRD0#5h=lB~&_)h|b?}$e)|7(ttfO}e z67I_Wyk*WrErsk^S5vwPymZc-&`AWUY^TtPIkbAYcpwgSU zXS7#n)t$&Nb3>;Eg+wOv`tw(7g}^{k`@z5D<-0?&SMhD;n(XCZo2#f94;UztXJyuj z!vwm^w1#iIy-s~R{ke3{wO+K7ubQIZVX*kY07t;1dAOd3Mg7US3r-k(#i<-GUvZ7g zgIRi3{5u!bp5s9y6u3(x+`OH-HvH_Q@R*ksc&u(Y1-jT4TEY%&oX~_|yWB@Z z3e+|XxUOU`pANO=Hgg{9Wh6+Ywf&66`~G~J6@d%57#~6=m~~1VCsw99px;7dwK=qH*^2g_A$pb2f`v&2q$Qo!ENW;p z^sQA*djP4OS{g2b|GI?6jsJj$t*)Niz#Z0wg7TZyd0pO^RsQ$c`d4vg1#Y2o91Z`Z zLX)4(fd69uT?_}$)W!`TO)by=2mwHss zw*HaJag|brXNV%aa14EE93G^xXDRvD0gw)Bgx(WmuwOA>-ovk7sAPw(iofb@3yZlk zYw8x@+h8f*XdY|A#ypcc&R=njJ>Ie?4*dDdoCy8IQ;_mOkAT`cWQSiV&n^Zqf>dUw zm}NX}r~UALN=eIQR#A(Kv8sog;*o+5MmFdIku(Hu=rgk#?nk)OD>E0JFF7%?SmBmZ zmDW3?cj?r`r*#NrKJUhbZPd{fwO&n6k(WmC9_dsU7c-wYK@I3=55^uf^WM(#q}nPU z_d2xnd7s5S&?7@t0+fL69^5knjXgw7LiKqHggRHC4mr~yE8EsmY6j!F)Bv@X;2stN z8Pi3TL`+-e*@$=d>z(E23iSpp(3#;k^G{Z~n64IMh8`)%99*{xXhMx>4!J=W zbJp(fa}|r~Zxb!zNj!vADhar8IQCJNvMLqe5g-&C!kj_p31CSb7W<6ph?Ts`C$2FN z^Vf!qxtNZG@xg2dA))fVOK_X`&jV~Q4-jrS`-2jjVwk_!)*rm8e%ASiRz5F(i#F`g z!-K4vsuW2KvpyGTO*RMYZi70SBE?CA$dPO?T@HE;7acUF4zw<&F4F8DZoL>#UuwgxbLMW_>RkZeB_CZi z?QX}cAOY2SzRFhc`UX+=nN(P0{9^Rsb8tl=MS#yhS2pDYEUX##DJDo#Ws^i_{B=UN_|9gW!VKC_!`=zw=CG*xDxFV6mUVY}38eO!F`r zO%C4qDu(NG4Na%R=d_5LT>}?zl+{3n;LrZzMVD4q`KN;2c9xTetVFK`#a70$vkj#P z2c3D=J93yjHb#_LS0ma!{{&1TgsO2nL7voP8UsF@6&|GA)a**&iCKNKV@+{zC@;{_ zl=U862rb3w2e3P&dP^e3_bvd2%G+l3oo~i$MHlDUDh9}S%LgnciqLaC&{|ipo!)2f zl&Lo^^BOixOgJRvuZwyiQ-x%V51SG$gBu{?7m3vQ}H`a&$D%}1) zkpLBBL;)aUpH^X^iY2)4NWKs;FU&WJ`#}txyX2mOv|*vzob17Yaj0T9nJ?yEFqYR? z&^x|vc8;ZhuU5hmN;D)?#w>$S8g}YwO zKMzEyL87#z%kddou02wlmPz3~3(o7t@6vB|D0MhqU#qvtyeU@$@5mL%Xf>0H^~|M5 zU)X4R(4eWhaeC|JGu5kMgfpif&*eCCfmjisCd@lemLPcskI$5q$VY$}o}w(6kE>aa zmXNW8QDx@t&})3&#%K<79L8P*1~Sl~NY=O#?)cc=Ud-%#7@$JEhGDaY=O{QlUlzDL zY|ES2)QeI>F zK}GBd9%_sWGcL8Kt9<@ZO6GIX^bX1WY1^@{P}H#&vvGNLS6JN^0)slsL`!>GgsrVyJQit^&*4PM5ywo0?r`OlBtWDuU-N zaf2n*KCm|utFEF8!}nVaq`jx2>Gi+Z$I=@qOYJm|^SLmM7xej4U&@9=MKCW3E3Z?` z*iRmPqPZvnc0u`*sbyIiO^a&NiPqPUZy7q$zAeuVXDnxL$SB6sns+3Y?HaA)UzE7? z{D)*rSW_B#d6^CfaZbzUV7rbjzzQ`*EZV3t=uCKr-}Xr8+>NxHFu7pK0ETXvtxO)YiJ) zcWIoalTG>ows* zo^B;jop+$qlU#3MZ}NIE$jDZkL5XUGRa1Wr2w{{H_*#Jd@kFfa7UW)QE_heqUmsPs zB+i~>oqE*jjfQFP_QlDDysftPQ&h%?jIB@VG$1Tew(Ucz$O`|i`tG-?M&358nGZL^ z0TC)Lj$re6t;yp!9Dkbr$|t759%@?EdukZuqIy{c!ak*V(^zAZLEq z{-xTiqDV-->>~WXL_jr+8}~PS5(bU~(#h{-?&KBh?A|h1ca2gpy;vumg*MYfeK+CT z`a_G7$N?))FIC5_tMDE;9C1Huz+tN}O?&e2b#ogrH?FW{l(|pm82kPw)gThZdU&SZ z&$M5P^HZ1^qgTGJb+&)Fqg(g zcONVJIknJ;`gxmT#-huM%cDE)?Z6fAl)dnar^q656q0hp%GUhTHNjJg$g9GdJ{b|) zjS%jSR}0LbgW3__Xu+L+s!ro&dW}O6+-CR0GuYI7dvbRQlkf1O14yywW^Wt_AIbyF z3^^&PYgGT5BK~ON!Y$7D0tSm5n#=nF1GeIq_q*$_Ie+bnkebmacJl( zw7j2`LY@7a=#f;|LA-tbSZYZa`^L=Ri_5M=p)1;M@6^DM!}W8&j2;ag77O|E&X z0}9;Jdk?b5QK07{-Wz%Ca-#v<7|Ie|vx2LlCq~-8hEIKmgOc6Xj9|n_?kjO^)hSDv z{#MEZJ`mcZP9s+zuv}=rQ?6qvXd5nS+-2l3NF_DxT7aXAZF}eDp3bB3Q)gg&wU^}| zuk43iUm&S}&+}$l#=s&$MmTfBU3!QHeiT|q%>&okbgGP}n`iV+T|~;C*oiP`0CGmH z-pSi`s>r0MBc1BEf>~G)1;RNvqm%(ff!bFeZFZEu_}K0S*L}OWMjBpmuO1OOxF!)4 zk#fWRjo>4XcOI7bI}{+)WAVI(tskIKt`A)^^BJYh`CK?@#TmGuKDL42pQUYv&#c?G zY!=jB` z?V)wc%+L4~uwS9IsSMG06Jti+>T2Zn%P&-kXz6Dj-SGgS9-jYU0$sd3aJ23fJJ!8< z%QMft)NVp<^*xxBt;`lq;>#Mg8>wRK?&c9-4NVsNo`$RwgdD1@L8XrZ;b?29HWBd* z1{%LyDQ}i~*}D&53LB#t?@=e3Ff&5j~mNGLPVImS->eu1lIQ znERvTKkJT$yYj5LQPGH$@~2WRb&R}Uw#Xa*NQ-)Z2-+8;Xc~NP;4gkh7y$r3dzsHi z8%jd_pU^;_4o)eKmaDG%ei$#UYYpL3(Cd#jXO8ptj~*pb-4m{@vydjBj(InND4VIb z0bd5Gxd89moA#iD=e>P=bE1uy$;?Q#1f)m-w+KA;AH!$~CTv*x4Hw@sv2-V8s{_?8 z$8d@>@&CFzt-yf%7pY;edXncK-g*DNJIN!uzT?VvlE!x6t6;)(Du{W(bOoHRS!EZL zvRcL;1_ck&1@)GYv%x||f!Ibdpmr%7kw)NuR~3Ou)l&*&I&+tHM|XwUFfW-0tk*qD zz0K|)>i*U`?JZEdTKxcCOGYCrORL{l@M3lssP<>YodJ=L zdB#B4c0jb^2EYbYH3@_*&$#O`jsUHU8O;ogYfv#&f78#%EZDzrGb2Vqb)&TU&UkKk zx!DeQa#2FpVqM?h#<{$&{L$&f7|uBQ!2-(qh$?21A&|mP7l2Zr^cTHnr+XHMUW?pU zI*F=NqJfNyUCcT1ovFyD)z4_P^Cj^5g zUsLh%MDxIuDpI4y$AI2$B76CX$|`;)Ef^UC7ExJwsble;B*?`=Cq0{PpW`>o1H+)5pCR zX#71k^VSNzBDGcrP#iF*?|}*_DLKE{ZMtO~zFXlJ6_Nm1*R+4%DxZ3~)k^IvU1>*D~j%PXxaf-W}p zBQo7~*-#EAX729L9y^iRrTm319F~!}m~#TE<9!t{4M)u>q>^XwUgL@rVNH+yt-5k6 z}NVKRmD^fh9P&1{C{X6k3syC|FDgb>C$WLwFBLEKec1F*vwOxL}KEg5A$;}@T-7CXbQ z^)vC!@Gj6}C6x|J4%<1D;74A9#5&gLI!dpcY_a%QFo#VQ!mCb&;-xr+Jf`3w-X3>7 zN?!5QJ*WIFV1#s;4Hs66O9SmIf3Z(-P!j@-Q6Bc_=G9Es6D3SLE{M!Qiv42{0dr!y zM_GpZOB!Ni016V@lzJP`X8l6>;tA(jam~<`k#Lfk-PZgAdtf@{#-U>WTi?%DgFvoy8FE79qUoFkVqu#*emv@cr`^WX4g)9V+3AJ`cT zR}@E543UJH6J2vjeypYOpHr3Ea&@~e8J((T*$O__8F@Mfd=173h&RUwTXonkg99_?2; z_bImw2C4itckO|b$>Qpd#x8(kRn8OpW*5tNAOrqvroopAfsZ7*kw&)YiLWEKZq`#B}5<_KltY z{OWZo&&72pdl`cKhYmM13X+TuMS=@Anm;ljySCWfUdUOhcf`-V`ia5#KOD*y#e>RF zTeXu+-ULI8IJ$I%OqPuFzxqO_!zE@Y+*!nX{Rg>Un@DQ9-ISu+-xLBsFMlc$tH9FY&%_=Krv*AZ{KxX-JGSe$} z*wZmlw!~soi8zS=n+OVAOk$ttw2%TF22`a+Ti==;VDUDqWhf6$dmXn z4$Z6xZD$wr%G_p}oE|^`n&w+vSpXX?UCK*ormQKvvLU4)V=LN6ikqaJrVL2Vo*Gh{ zD0kwQj}q=dJN#kK_5=`f%Hm$3mZa`taV&UK;T9Fssh&SbhxY1(Qhkg)jPtp`AI}OA z+vNMV15&^+bV-#9_LTNI$JOsa!t3B$jB?U`S075ONh zv5HV72o)&5&ns$ST>1H-P+*mpie|zAxJJ!B&|)j~)I4cY2n2&3fjQO-82qOSUs)Gw zl0gnf&RTJ?-(#Q*=VBYx&g*z=o>oHSz{yciQ_}^pB<+ec7Bi8vq5N3}5iVnLW0_2p zAT8mEh!YLT*I<Jcxzivep$)dw8ry?e%MTi8>s74hQJUL#~f)tAE+U}X*@W-mlE6ba6kgt+7N$* z4%&Du3Hz2@ox6D8-3}aMjn^Yb-+a)F_@yKf`CTor2?X<6@>fVd6^J1B<(%F?5kmKV?pacxi(JY0Y9z5QD8%OdUg59BUqDp_7yM9xM&x>W?#5mdOsCNw;Mhc zC-xFE55Tp(Rsf%_IX&ccNo_(Jw3tyZv0-!fjq*xq2M-M!9J~Lco zjC?{%T8Jw+l>fY&7km~BjGt;uV}$?f6vi~0CO>gWB-+Vez2Nf0LHTIdUfodz(70&- zSWeZM&t_b z@gdHaD>DC7J^ypuNaSDq+vcVib8_x~mX)w#CYRx#){x{#45*P|yiw8_ts}k`szd+- zqYQbYbX-EA9vXCg@D4%L?e7taS8xSOSJ?W7UONZ3jr+}-?$K<3$!U@NDiICTPfhqe zV#tTbq&}@iUO;xQYfg)3CKZH82r;&Ffe}X^MLSBSOVP0KwcMP{K|Pyy0}PblUUK4V zq9t(^;dJ#u&XA(}rKpL9@?Q1?s>;bImYBiMOZxA>=v{8hxAyuo$k4sK8n-AH84-W; zdfg8Z3Mf`$W~mq~kfhYQtx@D)*2ShXC!?J>&;G0jpiQFu`l8rm{oCtx+nV;|N$p>t zV)1qR3;+gAIry#2x5@=inv!t;3uEFE+=K(x8KFE_sSJ70v=pIW4riV1_UVO_h<`P! zmg`?qY)#GH-aDS357eN8@LiZ7S&GSOup`x(j|%(p;V(DzeA2Di!DNB`N^JMCJa#as z5xeVDH%#`Zlw@?=Q^k%~YpoTG8EyYYGS|P^1FoT+^?8%s5I5u$CeK{0VB5D=i;WI# zNaK_etS1jebQGh@zKykMYNr9kGGdTG2VgoB8HHDvn=I)wKcPl$UHHgYF2&W`7)PrL zV~TfzMC)v??Eqb&=;6aKj2v3z_%q<%BoGK)6OQvwzb?DR=}6|Mz@aA4g0uLe{8;ar zmLJ!ZX0ht+vdZ@FnOJ;B8@~!G(7KUBQ)*?TTr|s)sPvN4qsOHl5>0M}pa^WCwDqFB z+}Ui%8v$b?Hq3Q2y9Q41TH{evA zpyzx7w#2IaaJg~_l)peXFJP7vf=rg;iZ=gH1ttCmLw^^T6OgWYCaoj9*Plu|gFxs) zW;5n|ieMb!GpO!LRTl1b&`=&lrB)9-<2QD+-dtl(HVR+K2KhvH`D59RaW2)E836!s zVQ}WcLrsg#oElOv|PgP^E%Je>@1(jY|EuE^2qqr|odBS=uyxy+mg~U$l@SW^(Ez;(O(eLL}nCbQ16m z`tK#Vo)Z~4h}=xn44t$(SW3C9_AkLsUYAM4`KW~{5Y%rfrUylr(*wl#|n zS7gU1f7GXWcOlTTJSc~@ilPtGoy^jItnp@bB#ox*JZpVB^oY0=rt1Xy8$-k;KYMnw z;;~ouU5woCORd_erbYJ)SgcPCPPy{i=El2PU`N+gR~40rK!YVs(&G~jr-zHF8(sRt z-+1ea#ibpq-5u|-`vG6)pr#c5heW|e zUpFSXTbS3Dz?u)NxYN-}XJP33b=t%DlY>&=g6!4qs_zNnY9nafkjh`H!f4=^ccQPY z?-~IJ`YqJ)JZG!wiRK+v7)DX|c#dH|qz_-W!Ha|s{m)vGRkSwNG3$W87{{cFbjJZ5 z21E$r*Ixcq-;yw$ykN)qpScDN+)~UM;sVZR`NX%%pz6XpKH!M6-5Jxt))IF&=tWqN zy7b|e_hS8-OP*&Y@=}^@N>AhduM0Q_%S!Th2-k;yb^HG+WI+`oS4D2r>$fZnkPak- zF_^r*a|Z(HJJ$B?@FeUsWc@O3#()R*MYkOGUU6jM#0fSZtXDyMO*A=+DOtw&^yq97KM=Rw6p-fyco4M zn$M{M)UhNjO7y2fA&9E9HlOOdGPkmPg#|8_miu?w-@K057SuQ3^QO;7J;s4j4z52m>X4(YHD3 zv^uqH0hH5kbywh{_S042P5_I!g<0ot%g=<`28~)q>X8GXNZY3pz4%z)QrjLU5T$KS zH;rrNGaW_CSa3Jr^}9fRBmasYl?`K~5{p16n%muddH%zsn~uYYaW&$L^zkQx;*{1Vc6>BXamj{fo-q^$jxWMT8dW zL_4i&TqOHvw#B&OfImF?!}WMxz658mJ+91kS_!~bG5KHXg7;|>efw)x+?05}+Rx#7 z9r{m1{gRRV^sOJ}<5CTy_bO`xjt7o&&ncEncDMK_n_aiu&F8i)h}tm^Q?y82tS_f; zMVHDzmnh{o{Ql9c7l5AT7e_rX$w-F5Mnt*D^lUd5ED2W&h&y2X$1TpC-N$64G~ZN7 zOa!jl2B@Gghl-O^be11f#*bd@o%X;&a8+G57MC#)GAAE!kZ|d#k;1|K(@anQhgaeQ zlOXlv_@{v;tT-L%TKYFid;Eawh=fkMgoPGWk>bRcHFK`MU2uhI4V9lHWgZ zEXdHP%5ULYdm-8|!aX(IBFo6?1aE5IcgA-OpLPm5t0URdn{JK*U=S&^pLA}Y*C_E> zzerS@x^2f(A@fk<7p|XA^IkaYFG@;$?lZopR(z*K5*NRc*6J3U)SThcMMClqVnIO) zg=AAOgyscMFhUqMVYAl*TA|3{$S^R$6zTp0@f9V?umKVvaj758Af(ialY@$jY<0sK zr`AHtmJ-j$d#Mv2vaArm2<@c(wB4oj`-sOl%1ho)hvT(K-+T?$#G`bM?*-)E*Ac37 zYi4G636bb6yi;5Yfz5&KB22TNO+bw1z9&L@AA<|x#sGQOd=?%14(bf)9Hw8kxV#+= z`u1(TD1kf7K#X~&qr3kkbD{rrln*hc<75u~%zJa|=f{40NAMi?Qk1Ww20b8WZrLV^ zXO{K*yLs4`aM%YvWooD(7Nn2eZXz)b+7wOFT#Cvi_@&(CcJo&bi6`N4KbLLD#J)AC zppP+iOjkLWqVX0P(D!0%ASE^v8af=v#=L3Osbc%_&?s3zfa&_;g<76k%`t6Ioe%}I z77YUuGl3e2t18IYoeCHflfp@|B?%AEDW%ZIKf-xk=0|3t)^(5be%OBVgo=cn0l!5| z@oJZL0j_6ReY5#gV74Ct>9DNCP)#f~b=F0cLwk9%V)PUacK~kF-JY250H*P+oHw-U zrH{)LiSildO}+i)HNl3&D0=`n{#@n&otD^4(_9Oh)whQSee+>F$sNz8{&qoUa#E2- zgu*rkgkRip8!UBYop1fviu^=6z^01*s)GO5{;~Ku(Z_P>06Tyd!C|PPI$5!vUAM%h zCH!PdjTH-#1Jl4_!-PSl#Hc9#`%%mpOw-LPpjGqv-+%w_zdhOj2|Od4F|guAdOcW} zYd=Bs)Llo8lVqFXA#3i#Ll&4pw!d&C1ZWaQqX_ z$9W80;)RNyK8qM4iUdN_C2I(m7y3h8D=8fX zXn0sotG|k=RA4KN)`@c8Xisu!ab7P_O+J&kjio{ixR6%rt-Jr>4GQK@T2& z)9opWjT(ndjWAmOg|Ceae0y=5jfDF#Z*!sqL;N@>Knkrf=yF-&5tE{Ac-)vHyeEO* zl1hlflwdCT=)MqgUJAmzi?j)m=JMcSTu3|VGEwJ>=b1ZccyP?<=EL_YpRg8m z^kMH|!t6wAPSi^$zMgUM3Mtf?v{dFf^62*ya(8CbpW9}T#hWm$ca`n@{G#vQw$TSz zNG03IR47k(UZnIDQgp|Bh@8YgN-UJ6p1;D^QqdOL=8`fkV5&DrT@HJrxQ#Rz{`XnE zVUq&F7eB3{rIg@+i3{ZrS(^@K5%rapT$r_V%nGDgD<#4|(<#Yl7~Jg(R;RsDV=hH? zl?rmPi_nzcbDX3C-!Ow_rzK745Q*%H_1hzGy6`z$JXDuiu)WvH(7eIjmXL%Efosv5 zDZByROh)P?7KQ6)>!$^%m|33*KRmIl9J;qCS^PqxvZ=Dl{I$#-XWMJ z%X=exG-!!`ExN+a4;)+ZW9~!3VznPN#mU!duElw2a&V~@<=BdU&sWmED`sR zDp8n{W}|_kh0F8gJIw{|68G)u?c`Un<~noEFxQ7>o=a1@!(T6(I`m2>c?v9tK6cjI z2!8kB&PN!V`5I|3@^v}DML8uj`YAK0?)wtf9&5&(KY2*lM5tJQk!N(+WVJ#9;&C@yC z>Am(-wH)mJ!#pcGNxfu^Z*w_)fOcEMN7D1t3_cexSOQIKl=s3o*0OL z+fx}LHO#-Q*{~#4(IGiXB+H@tib9s2Dh$a#hB)ku8YY!Vt7^@ne}NL7Ea9yiB#}a4 z2Ild)Ba@+qy8>hNGstLj=2zIqW6_fUH0r83)HYPQhls0 zZO)e4R5_VN4Qu3|k>%ePF&(3-!Uu;bpW`9LXrX;lZXmH^i1J^mlTcl!8keN==Y(6O zH=xUOhyvNvVoz=ON9kD8pyj{R8s48p#|v~_k$AlGFS$ZcIp7pP>is#+)PpO&UM^}N z(Z-*^Ba-d>_@OB|U7xzl3AWvTT;;P1h%WOS)alny>IK^~66C zG?_zRPQ(O!bNumrpHV+|bWx=g-x;kJiBq}oV99_8w%++VQD-L_|J;5pFeOj44_86OPL%DFHlfM%x|f|hw(@g_1KgK*#@ymtYKk9USm@IeXy&Yo}q)BOt5=u63@LG zS6r7M&~w}DvBC8^`KalY#2=>$UG{1DfRSQu?(F@tAqU<5R%vkW`kBh&sOYY-j^SRF znY|SHke6Udo|H}S`SyaidZ8lp=jL5&zNZWW>SfEbZ1wU7>ZT_TqwFRX6g4OSbyM%Qrj||BtV; zaErQMyZrzI3`loLH==aM(B0kLAq~%O6iDVE zhooI}^x|b7sm_hp_SX~77i&}wrm_7L3uIyFAy-lT*lx<*BM03qC#-f4E4AJ)%oRf} z96QQJrgg_f!y$?ZH8(tL>^WOj>5efnRf9SGuEITw{AZa z&S?6lwvT;=U=1|u91y@0(D!6g;!P=_tv(lFTB|;6Bo<1ya4>zGnX5-oylU+(J^+31 z@C~NZ;0>JD*Gxy!gnoaF#j$?oSfYCpm~6dQ(%+UjR<&^kN|fedPSJ&Wqkqef4;wTs zH8d+R^oVppS>zPmc^>|#>V$)Mi>qWDF90A0oMA?Sc!$wD$rkoOl&<-s&jvo|mK5NH zhQsr^!p$!K#51fU%l%w6xl4483{a=hA`m?%CF}9*EBaVKc8fI1p|OxbP12T3aySEo zSOiGHHDnCp`r$eM>9azHR|cDL2d3QD5wBppcA zrX!bHGoqrK`Ia7>R325yv-r-2T(^T2B-_>NAX$Srnysu;YT(m#^7GmkY&rm<)%FKP zrYk=v0$NpO#?1;Bt+h(;NaRILJzhVc0r*e=&*J&nVH?i*KIr7xl@{a5R~4(uJhw-0 z!92+~#jv9cq+X9Gi;UFz&73T<@C)@_NB}DY4Z~~I6#eXc)`j}_@Z>^tdFq1cVI_CE zi#dndzW%Rff_=Fm*#y0H2FYmQV_emc*O5J|( zQNSTSglpI|1A|Yk!$xGt9veoSoA(hWVFGh7AVeD6iZ+}=XEA7#-~E?!D&+e{@@q^pJZ$~}acq_au-g#(b1KM)#pb06?XR^ZbQLoRYB zC+qsx8cn`|tJC-R(Lll#go7;~18@3Aduq&{hyeY2kR)5(LkFS4Q{CoUdDxAI+UC5V z+m{#FCM$7m-(cqMLktbF3Pe+?*A*77oEm&O|Bxusp*^qV@GWuGZ*%Om&jc)fZgQ`G zUpKni+P&xyLvDLc&0##5Q{S_H=1M1Xi%$nJ(MnUG?-sxwbu3k5n<}uA(~NoJGxSi) zDxzWIc!RH;4D~60*8%O_eY{dih&MfD3OG@TQ%IuysEr^yQh$FP)=P^rVY8n^fQltsd%V?XntD zc>41i7JSc~G@8{dJkroEhs;DXq~^rD#6>rJ4-c3f$#m=$!jE`+f&J?Pivs8PP(h8x z2Zb(fc`!Uc3BJg^*(_Efk&HAv-d7H$?_=52vp9q%;A<%F9LrC}P%cLBwjh8b&cTQD z`vI$hrgW@2@KI9T2Q|ebgJ3=?aJi9KDs)~x!Bb1p!k+XmEVYy$&_1kT zrG#~6#y=Vfi_P!Sso>rbl4pOI!o*CSuU@WDXSldI$`!-zU+#CM`OzODDeA)hlBS4- z2Q2bkOY%1EGW%uNJd=-b^6p-FI#_XcZJMi>In#vn^@8Kf%3&n_P*khz&HB1VCQt5* z2Wul&v-#vSA55w>wws7zR`!Ey{MTSVl1J|Fj=8Hz(y$>?y-(31;NV@?ys?%8p>M3x z=|7X-Bp-L{mwPSIBO&XK7k(Uds{%qrt37(>Z`V8$-!-HDNe1-fCBgQINv(f=Bmbdl zy+SbJfy>+=>!nZsZB9NXVYXhR(^yeqCMEQpbHWZ&_N+h<#Ra^cKeen3z2L!-4$3yM zf}Bh>I#*lcV&Sq$4wJ-2D}IdRsmwDFFd+)mvmNk>enpP-S0?6uU3k!zRj??S{S9j6 zlLKVLMbwcFLC8e%p46%VgG@TEou9>0&421rLh-C3H&F87S%ShWt@-EK|yz%`+el4HG2~ zE|`t{2riI`bG59J&2w6+-Z%g=a^eQpEIyW|FAot|#Kf_>(}~}P<8bIL?nQ>}jtJf- zu04FA=e>hIB#W>v4J)c9<`Wo)ioo5V%X+$?Ev_7N`#^+|*(CqcKRWtOQ}Fj{AIA&n zhXPTFg^_oNU^E_|C@?S1h#BRckT|3=#)Qr0gS;G7P^Tqdji^jML2p0AS=ucqd~`De zIe8|7ysGp1UO$_C=-6$lcd73NG0CItQRd3HLYmZJL29JFmNp+9qvr*4`Uf&ao>Z0Y zlb`~(@JfxJS7+Bdq}+kzmUw}Wb37izuM*q!>V(jMRml@S(yDqm1!Ei3c%;_CbEi2q zsEfGhK@=qA7UL{D?+#`UuM7i@5*#`Z+Hc#wTqtkX^GiK8jy9iM%!Q<*#H_`*HG3)? z1tFjk%_{0<7Y~4)eZDltr!?Terz?Mb%|vVy#+!yz5_(Aw`;{3@M0ivnokf)4J7RO& zNM&-xLB@#RBs9Hpsk*p*UDO=sL_u$e3wpYu?ZajZJ8Sgd`e}e3obUl1a;L-e)heUP zm>?U6Qq#r@0rqw>x*$my6DHt*Shm%Q7%OU4@{%!#jRxwWaic>filZk)`wpT4y$}_AY}w4AhC{PWvp5b1&51 z**4NFRQOGL)U;~3RlTDM%(E}YrnCoF=SrNNWRkes)s5 z+V%yOi<-&|xZgKrIV&eW6>u#$iWUdEqXbMMq-UqI#vCp;*|Z^&1_&Idt|DUQrvWtW zY#i_uGu3z7CHMmvDE&RC3!0<{QGC_Lq=>|%qs&tD00Z_{Ym1JLy+qDzg$^+Lh`L-u zU9jDtq7KyDX^VatN_E4eNpB4=dTC>>@8K$LRe1wiPVQ*p5FQ4%+2QqUw>X)a?Bp1Q*H}Xn)_(AjPpxrCO%JK_*c+doy%O-mWqj4# z9zWDp<&f27-e?>@hnvk+jWkoH^q_6e&{W_=e}bnxjR3BAAK8mlP={LpQG3M`Ou5f&VigbMpvFlQ7 zx-h4qYawP^@~Y`?VwlGV9WC5q4mg6KZaVOb0ln;CCE!GD^UMFh4~@V%TC^$*GcHPxkOMQ zil5+AVN3ArBfm0T#kQ)*+?DdA30&@C7j}`geALu(^+VtE6V$oba~`5Sl>0Br7)&`u z2wgUp3H=+I{_7ZJ!f3TyDsat=#NA<946e1^z%LNPd((ZD+pwKZ>gC_FXN~OT9Jm>X zsdea2j^ac_&2CV=zgzBXAK@<7?B|9-JtVvUrX1nV;6P+_fdL7K-f=cv7zW~Vjw;0q%-Su8JL^rh6+Q@xxnFrtZiyDw+fy+V zx9Tyc#(KNFWshZ*6bgpf?4Mp;%SgL9)a^>aG6}&Qy_Oo_{-WbnHA&U;QAkCr{MttC zKKJ0=jum}qSCljs?wL0)&pD zp2@EY@BTU(S6vu-FhH`H5A`b!lL;w~r?1K$1=iw6uKMK6;2sk+dy4`>ME-j3SvGR7 zX8EaweLD;6fq))MO0`E?( zjjjg<4xQy{bj(;*@&c_mD$Ss8MJbg%oy;m^mXzRvuw_?@_ALqG32izhD{(G|PO*K$ zuLLeFFU@@(jyCQeRytL`nG2%$*pr%xUK4TB+Qav$bplVZDdsVM}2v7 z47vyd+I41J@(tavNFjkS(?RK&d9@cG{! z1K{)%e<(bA$g7LPTGNTYZm43GLuO&Hzz5vY0!~0+xeW8Nl6Kb||H%i}U)1`#nMqwo z@8r3-RM_p82k0_G-` z;M^J;g!AfdIcTA*rg_*wpFaR^7ONfSM@Sb>MO`)o5!VT0iP$F5H=E>0Z zGt0;ben0_q)w~%jf*R>+PKGp^kg9dpPt=Fvp#xKRXDz0ZW8v#Yy{EC6^Cbf`cJmJB z?EUp=aD7-XU&CCBv6bP2y)ir^#^$R0SyZ`X4ZUklDf=pOTT(xpt3l0; z6L+&<#+(z<=+3+ij|7E(2$TO5|*;P$Et9Fgte z!gzXvVy_OeqET6~qXy4(f?HWZQ(J+9`zq{R9kG7g09Ko2&DHZav;l3+1O2(KfDMax z8Y5Tox%Z}Y7w`DyX+TcB`KE_VfS%sO@P}Tp^ihk3rEWLVtYy5{dmG>@AdeT;2;F%D zqobdq$^$*JxB+_dzF_+^OJ#oiR%H~mY7`2NB9E6n6Ivg2-s{pYbG-2|MDcZjOz|0dy;vlb8%Ppz7=UW7rA8ne|`0M~Ca zQW`l9JmD~1qUF$3w@E7hP+jmwI5G%_C`tw)G;i{UTM}G=SsI3s(cEs6g6m2ob_-lZ zgZJsYG{kIAmeX4HO5_CrOAD0Q`7i$=MsW#67-HU@KH z)+w<~Ek{{1MC2X&L{tFAr@I%U(8IqmCm<#g?egwPv6 zuyCaWcR0J1V?}LolF`8f4X7f2%0vZ6UxH>QzDma}+WD}^-ymLjSH6T4mT z0Tf0D5g5c9eO(I%iCB2w&gw_)ZF)NKL6*p7HQ3CBri=QM?r&(9^0ZD%LyDrun;#r} zDuVk*hi$qf!KqySTkq^K)N*l!u|o5=UBL)+R#?u6^*g|6{GryOQ-?usx%?Vu$U;Q?+p~&Yat$EJ!ntY4{KI-4pN@ zc4u#bn30}W|D`ROMv4|XF25FILbZC!<@wmie_KTJ!@%rIurOUmFa#hc>xdt%4lO=$ zA4bdN`z*G0{h-#P0NyR9h02W#9gkIPR=-ER^5Q34ep{Uh8|)J6%reLH{*uKK>%Dgp z{gVjYe)dzZ{8_Onmf=~N4_ZVl=Ho6@EUnyfB6}m{lCU5bpi4s7M_yuiB1o?TJnUss zpc@&6e?J-ZG^R=jtDJ{{Mzdm36?RC3{zoUt6+oV4zWxxOAItE7*4akB2(2B=vgYp| zx8?;cgOj3U5-Z_?11f(A_13_~55tI_pvro<^{7-%MBLVQ+F9Egf}Ua+IAcM4K4Ax8 z&V$52H0K9T*q&8DSH%>aAC0>Fl_X7Y6i)+kkW{Or$a!Q;b+SuA?2o47Lcl&(x6q~v zXw5wDa_EN9q}DbwAj%e6cq`Z@iP&)N$y5IAp)grbq`b4mhnEz&_uz%~42bs?{jI&y zxJjNlfd|1@TS(%3^5K@nyuqf(tqSxc$IO)nyhxQtVWza6;GKs9HcxF^xJgl?Jk_!I zs|SyuZkrl%hQRanPtpiH0+OFtH%g6^iXnVleeb_i>Cg>I_SUw=+U?asz4_5AEDaq_ z*#L=Uf3iFkPLeHnx0e3-`r8%$)$TMFPK~)Mx;}5zs1xb%Se#HpsQjboITN~joxE>j zV8oRDWLB{dSGp_v-5%|OJj znqs*LI_Qz~1>_&afx|Kk2f*c=QpJygyDmZ~UW&)E#Ad}mNo-6T68|lHp2sk?>+OHM z0odOi$7!qtJTtcl7&2-{M{cBUm~@I7ysRY1$XJSCy`?>NTxY=4le0u-aAK*L%f3!2vHoYg8_N8x&)rzwEg2iLryTf%d3j46oQN&ETSggYmSvacJQi zwTOK1wSiKXQoMi057!|H8?xfq{Dy-{&D3cFdCT>*Cjlt@^Yn#k$yPORO&S2#bn2x0 zd7sw^lV6rlp}2LQbmPKexyZIrq=3iBPlsT^3@XZgYSWuGDzM(^4uvifjof2`(`O@* zIpQj1n)tH);eN1<>T)hH;8oqYVwK*XD(uN(WtkAu>QQ?63OV3D&a3nZ7(Ed{-oB`) zGNBed6`A2;nKwk5Sjnn(tSIupJrzz;Wv`kMRHy7q?bAz3JMjb51^eK{h=Vg-Dcxu5 z$MLRuP8zRZm^z8ngO>kApP1@3NKi|;f;?SUbRzE|R+A&qCR4sc}Yf_O|`M5ZthubnoX z=VqDA;sd3!qFXIV1dz%EBE|6^w0)-V!%1`-syq{5tJw66vdGijXqY7Kd(CYeh@b{j zZ1B4UBv^Xh_>`2=7a(XIIoY(ADjSb;N=|*yM8gjDS4VJzoPEl!)8jAByDF`Z;XL%| zGVRC=wb;||0LFpfj0J|auY`09%{L_KW1APo^x&lYe8&|IL9`$v(8*e0kE|$+iC@gg z(r)8`(OCK@cX895jQUx<#ay-=PADa&Z=^=^cA3DPN5wEqkRD)DC!!xXDCawEi7&53 zw?`V+PL_4ZnK<-RStaQLeO42Jwd`xg&T0|K`5%l(`!)Z*wjOOtc6<-Vw`ss=CLdu2e_ z6hT7jlw$;g(Z#{z{ZC?mjd!+Mx^Z&G4wSl~%4^k@pWu&ADmzk{yCSedXV2*FVHrm4 zq}8={Q9;dBs(EUt+apCcN%nOAqh@-XfG7IRx8y*hVh3%RVyA96;fEz0jRb6GbMjv9 z7;6r4qh}Zx*Y8MwV7Fw@a(wH@-}d0I!4C)284Go%JVRTjx9_>W!)zAG&p*nnbmCAO zk3Cf6iAH99w=VLBqnRNZiCrkK^wCsh%ZI$4z$v5R__pCG;lyl*t{fN{j{WV%2&!f& zV`Ze1*$;dyLTnE~vPCK^Z+Y>U@ES#JLf#eq5Hc*cKz&w>RL_(S%VL#K8#q-mlq+Hk zKxFxRiVPk535#XLG8PMTc6~iy9px`EPsiGfJGI;5j*p*00bkLU>l0lixl5afm1Yk= zk5;_A4l5@~yfFi0jw5x?s^47F`^ZC3ud%@a`M$(sWNawicc%r;c_SUqjPI(RJXE~dUBS&1LtxFr02eIpV_KnQFL?w~B32To*s_pGiW59}(c`-I86jjCqQ4KE!RyNT6v)7CE84x=B^P|}?8NK= z`%UwLfp0zyV}rf72|C{B2$GTKtzh%A;vBK28M$$=u_6NSA=v95yy$9;=q9z+tu{78 zK991H`^%0Ir442H6xMo6tF-qnggVuvavqMfZ!O|WPeJPu(IAdaoe}QX&U-)E(BfcM zDgZO?&sw#i_e)vg7HteL;>K*5WOhYoEtHFjLMNdz%kT8Ahp1gs|8t@}U;NqVULH9U zsgnQBuox&{GwSyb*ziBJF@U2ogQ5o?i2`w_lI#9g;P%?9_sf29rO*Y4hNd@4`4vzO z?RIJ7!_=}_<~wQ~G&CaI;2rXcOo`d9%w(1TAP|AvJPD}6g-Nl*d%>&09iF^XfL2kS zt5;VGC}{KzN8eNxy+;5XC5szybF7L7?grMf81HsuI_6?pl+i5vE3qduh~!X0-;;Lj zeoEz3Qwa6WY3FdSg56cFzIP?`TaaDNnx%oEgb z`HPgEn@PHAaaK@s$EALf9Gnyq?}_Ul@L524j_&fRepJ4Q5(A%`s&7ObLY0Su>XcBC zWfWmvq(JjR0{l1^#ygdCy7mj|5k%W|1UZ46sIb<+(%cI_XDC!Oa)~0NFCBpTGQe(A zrd-5EZ~3H=>>r>XvM%qnGx&tPS$e)Bh6_T)90Dio@NDM3tF{nHI;HE8X3ouYKFKe4iS)k=$h~@J|;Q} zk)2-yAAg9nEW0kT>7@k;w&BYewpH^hm!iJ}g)S+^wTT6G({DvjpII`>4If_`u3|gc z$z+CrL${pF;94=)E*EvIbH~?4BCNyiZ~TPera>@2QMOj4O zJ>F*r&V3AR)uCQIndQd4&h~^vL=EL^y;=UZfY_*x>4_?i-i4tIfJ0%@JAR&6+1Xwj zI^Bd`DJ~s@o3nB5GRZ_uC3&!gy$8I+f`=--Pr|4d)1V7GY+QFe8xVp^eGj6$HGxg4 z$jVNx+}^^gq6*9xfq5VAb$1t&KrD-bRi8!rpNVH zknP)jK2B;y3O=ajT;H33U5A+riVU}#^Y_G?$ZnG%k zp&LD+X77-cenw6E$(SYlPb?#Tx8{8Om=bu46kq^&4gxGwFBSGJo%t4bOfN(o9q{2{ z_wGHcTfwCeMs}rTKZ;d#3AiPBGspYDqYxYYb|g;T~M>NqO)DLfFk{~}NNeLUTAEEMzj z{?Mq?+v-U54bV$U`9`Bh{k>{~kFP`2U5CeZq>F(7T$>QAukPG9 z3#J7QwtvpnPpNgJIUKq`FMce8d?H@qeH)wc1J9(=IukxN6NWo!iicz!TqZ#?aVg|Efr+MkL! z5B+RaA_mR-?ib4run%5ccV+k29PXsS<^RTRfr{m%Mdanjlh%*?Ubw}*;TnuS*m$cR zdf!~Tl7!6l{!N4ZwW5Pw-u&78gzW@(tpDzWJ<=fQ;OZ6Pe*>@oVKByndZ3?whBd1= z0o6S$`6cW|Fh=fE+Utk7NS|)IQM~qN)zbJBdmkgwEpRWA+2>e#bAdfj!^<4X#fst( zK{NiGB#bpYz!u-} zMlkz_Yxa|%2U0|Ue;qaAxGM97aAnXMG{O0-f)XYBiQ-O9y_De7CGLRG!dXZdWBlHR zA^08!lk~XK)h&N`dZh(yXb>m8hp}1fr*B%3&Q;-qV)2ZQD=YqSz3k$Kl>j+^<%q&p zn&K9wGJa6y?MX`&1(tn2%DyA-vxw&Gv3SBqU~X@0d8lgt0MBIm~eC&suL z2C7&VYN%M?gFxQ}Zc+Fh-+=N5+b7uqVlEt*E7h(#jR|WF$Mj1%rA1`8{uSJinWG!=GcAITZRlIU#Snp-o>o#9T z4C7jFemFtDH1vRHvKaY?aos~e^@n!7!2F>_@9=gPQOR}Cq4Cp%%rHnztx#xPZ^`A= zJRun3@REaYHZ4DIOeuGS6;GET-$)mLUf(AOwT)zQ)W4a-nhg~n@gxaC(v{WdJ~ny3 z#QjV-+&P8m2_Zi4jOVCmTaW+xCk2yNP>=<=UfiuRe5`yjR|go)xn0aWdka3_wyL(> zk^>Yssr9Qtu_6txh1;-#*xnT$-o%E+9cpK@*o5V+48G>UC{=nVIlxwhPhD^(qXfJZ zCKTMGd%X=cKCq9_DD%JWqJ57wGomKM+8W8II@_LlyBg5F>&UxlNP9Uz=rdCP);J)f zwxQB%W{#mV00-1iYZg@!iQINv_xqjp5ED@N;t&&uXOlEZi$ddgO#Z~D#^8Z|A-Y-c zm0jfU;=(1Vs<9S%I|>xay0k26?6mv21RbL`fw!IcYwIw^J^^uzb2d~7wR%f9U5!R! zeKr*)0!x2Qztmiz+x0QSfy3kd<0re0Gft!Tl$8i+g>=Yv3r1A+0=$H5N2R~DjVwhi z9LS~?IH~sNc;!{~mxYkydCq2)_gP>}{>68vF=J@*^F4%Dr#U<&`iWmXH17L<3K81~ z_a4>)phtysG}2|6xj1cY{ejl5R~JULX*X~t!Ofon7s}e5WmkIdA6)VuS?JKQpSHGI zgfJi9n5EP8H;rGx@gFUe(Y&U<&9O|s0#>Lvw8svNuuoto;Q;*|3=2nOj$K{`g8i_t z3|@)9Wpu)33VBHP_#INU)VxMUn}f3ZGhuSr{pdZ+lob5RUjh_5$F2ABJlCFS4i75D zQY>DqWa>V(^IHi1ugUmc@y}cwZ*z2+{TrO1!^qV69agtx*Zkk;T#J>n(_?K}tPy}1hQQ<}g$~TCr z)51)t0&b&!rpzbN+i+myW4?o$eyIM@DM$LHr!60+fFyQ(Npn|ESVEHIs6z7=$O#T~yzN{^>xgd^*>s z3WVcGs;0I&Lm))~u_6aWUBNLHrXsj-_Q^>1j3O-$QoMifd- zIOaM@Sw-tu)+*_!5Zsn5)7}ZJ(>;`ZN3StjRn~T>jin)0m5FrJduk!^S!bVtm$pcP z5ym+IE}lIQf<%5x2AJZfAma8U0eZ7JC!aFCO^67)YFi5k(6ZUOeemm}=-mdiQd7Sj zl3@b}zc!?zktkJ$Cq17l1LA&??rAEc2JW2Aeflk8q>g2;*7M8n;NA*-z$~shY;_iH z6saQc3wed^`^q-pWN}jvY`D=!Y#eLS3CS4}X}*_xZ^29DlA_`dN~}5sW^7JBoIT(n z+WQuVyFMdyoVE2C-~Fj8b3YY*{V7i0^#{2mz83ni1kG(4X4=A!B_viiTE^uktZ}QD zulFZQHy()s@mxgFeg~6iCNl)Fwc}%H@xQi;_DNJo#Zk5_anDHt_A{d1VnGWYgXoX_ zxJ4_kKn#G}R%qrKWA;S@gAIV!I+qlGN#J9IM)0&-->Z`W_}}p2EAFwzJO8 z>l)tauc4uClc<5y+=dG;nD}tlrx??PAV~A>ju6ezJyOuiILUI?;SN6RMzuh$rtJXK z_{p;(d}u)c?|)*uPE6VZ>Il~^^@p&BIU9|PkVyq{URf52HQCn`2fH!pM>PxLCy1V( zhX7c{l+ML?m>dY?1x{Om&+NjOgp|4HqyjS1LLZ1ganmS;B1>%=;iz4`%5AB`SKy2p zGlH*^lqm?0J^W=FqU0^cIdH(Vi$?*lAFO|KK@;sIEbO5z_bg)lhlG+W zvKXLfv@EJ^Bd~iO#Z^XC@2Oc(L!^E3h8H#E)TVtTsyQ+$5A?a?5S`KlH>gJKiex?O zbsFtD+Ip?gC_&pr5)s}+>rV$>g#J-*XWG+Sz1tQ=QTWSLpYl|cpvj)A zgowD%d;%BBYYHo;n7C`Ziaz`J3*rMUW_TZ|%1vyrAtZ$i9?pK@f}>OL7L0`wyXgXx z)b|T2XC*F)u#WG(qCcuIYn9NcRC~O-6APTu!m(WEgmEfRejaMmoi``*-BNwCgD%>K zAn{94z0v?0=93#zpp+TGSa|C$V0Y)$S0^SJ>3Tn1Ws%*8FKwgR{!5U z5mEw%I)>{wW7LQYe+`>OBZ}yMe1`wAPQG^sYQ8bJ`~b#Dp?)_@t0?f(?@B{+4Dt$= zl=G!T?a_i+W47 zrJOlYZIK~4jqkZ5YDV@ghC!%kXuhTCpSpkVya$6dUwmb+^6?cenmn2_Ydq(Yl}#)q zXLh@M;so(t?%qva+5Z&4q4Mcj^Lg)o-nMr_KtQn3BZXGt6BW6_`fyhH3P!?TnF`<+ zlH#3MrrRESRoGkgQe_V@oeNoGmcKZa7~&=9?FfI^vVS7{R>n|(l5+3+VWM~CE|-K% zbAL4;V9_D`_%na?n(wYp+;YIQq0h-&imU|MWd%cDjL`_SHsH1+t>Us^!H*BiIe(=x zgt3Mf8nMxRd)G|@h}-2XbJIeTM|E32I;9EK(4F7OpJinZ4whYgi$z*ZG{_9Clm2al z>4;bpgTG=$+z>Ng-q>q-+hP?sS9V21<x~ zKUt!z@9wViZWoQJwVy7hlnH?yn(Fx%;?uNN14Si{3KwBk$F(^tAXA;0xmk4cP=^Vh zr#vX&B{qNMd(>PMtbD~fTE1n7Ob3V*(GYs5R?DW{*)rKZO0|?`P+Ce{Wyt;2wN}t? zwP)QBHsZPp5KNMlb^C}&-s?2wIXRBdKfSM$lGZ_Dh{RXoO%86XPoCDE+&kRgc+fw_i{Q zs#j@asbf1XBn!C=jV!E4Xg;h9xH`zGmtz1{lpU(0m(b9p;vGK@gdlx?YZ?H+5WtKN z&edd{i-vmQ$Ai-+-jLkkiZVoyd!F)Gij7PJKg|$d96;M$=}kbbw=smtGQ-jm)kd29jhTb zHIy9RW@(HOS;fs7{g^O{ipYZxTmY!Gr3+E6lhq${Eq@ng$?Ci;4tO-ne(J}Db1a{I zecdvKw%pC*{_^C>MyJ3ibhEXd>VK2gZN6no{UWz9Y1*w&q7lrp(EJuL)Ya>Vs=SPA zQjb~-b2Ad^9Sf1O-;Y>>HbXM1M!GUIEz)10jh)pCfxN0An#*qcpDQol__eCaWN|JR6|r5 z-gqPe-vx%=nF*d$;(v|SrN^^S#qMJwLzdA#ejqSG)E^kER<^EiZMkWlpjE!``5 zeP}~9n;`m0hXiz|ronC3KaM8Xh{Xn=_LsyB_x*s%rC)j?Y*6b_?2~j~pg*(~chAB* zur654w7_mLy1oV8`0LkP-;hyI1nCVM!H1HWTsc;FNn*_%kYf_&X>5ajiU!(fY&AZ5!rZ-G#^U)%&OBYOdp^i-~2$CC~M9>`4Ki-K&=Mi35t-7_PIh zst4eWUD$r51SqJjHV~0_oFgw1ME+2#osybDn_kss*Y9D{v5f%9J4uf2QGoVG!Lhu3z{OvR`2%q^a9@zp6eEuEX$!A-=Bg?xgN&{@16c zr?k$s^_Oq-7q}4t#H*UW zX4`~?W~7Tme^^L-IM_?ck4}!bn_2SnR~o^q=OQXeD51Uc0^K;?;a)}h!UXhpj(`Z! zDd_DHe}*zs$f8c~rXT=fb@p7YfT=WL{)tQ~lROV!%>2=66mG5y$M#29M> z`5!QHqn!bGCz0NbMHM2@TyfW&EDwZw%v+0R_ZG_@2{QW4vE4$vK{C63toPKU5bkpth@RzFX8TB&g z^%11wC9FJq%6QW4Orkw@yQH|dp1E00YbI$KK=L~k{7jJ`Ti5kCJ{r2yF;`U&!0gn# zV1g#aR4@S{s2KF}KX-TRk7cEvLAhc$FVS{7KJleL2y^J^xX=^UmCGc`rrJ1E00q--Yj$JWJC_x}#ab zhoCVO2kU3;=(|q`0SvtJafgigE9o1Pwm-%S<)qaf4`L8U0ymYURBs9#hX^w)Zu^&`aQC?J;oCJ->TXB~#2IBKqN zUF-@w8i&eg&%V@M7U*KEyKpB+rHM4bsQI*p8K{Y2T$2i-ZZq|JLeXh)tW+)jelzcQ zB|`DH(lS6~uFUJT<~ps|b@~*{f*=lQiaWx|JuCi1{BjS`SYOd0$y2XWXwd!NKv57k zIuuWT*wta;RfQS|8`Yow6uFxgj3x~KIh_AFcZL2W=WRHF+H;ijRfdr>05Eyt))qYX zY9fzZi{5Ym8Auq%4Wa=pqU7NCYi5X7xNP)m`cHvO6ggw)lAw?}ej?n{dV)Vj?8C2s zm&4H2-Epg5<1eK!xtGIK*s}X?(Y#Ey_Pxm;>61h<0KQX*LQbbY>0W#x+9p5x(5&kB zHY!raN2x^u-z3$cBL;e7=mqrh8B!R#&%;1#P2rm5X#0{%JULgL16SsYtWcB=kP%dY){ zZMLF5T{jj09SE>eF25}WKMNraz)?A5`mTk3)G*N&ycT+Sj#i0^bw@%TK>?7b!dgME ztAL9RZ`Zat)i5T|t}Uym;oSK*i7@@KL6-NSV>Kt`4#E5%2B{FdQr|Esa5j|GUSg*K z>Ejz;K9XKWn)}i+u3bUI4)L-*?iE>$l~hyURCMlcqeM(!_=Lqdb!`YYK0Il4qxhV* z+euA!ickurlK6b6Tl*HwHvs;Xpzg=Z^94%6^$OjLyb{d4LEq$-avx(aO#D&^-uijR z1XHiL8$(17Eo{PqO*yIIVT9DqH>7Of=VyaiH_wgSY_%ZT+0htdtcc~68CXW=*MD=`e26^audBORa3K+PvV zABB{IzUGm?+885THGf$mX6U*h~)F{_$}WJnT;ck5m}*TAWO-ER2}Cjp0`2C&*F7-E2Rf$o1CXCT!f3 zW9p;0uo6^1*q8Ig*U|_EOGi}(!EyxnoUYGKE^w@xU#&|QCiKD7*Wmiy>-S_T(gyDwdI^Uss|&ILv7gGVB?EoM*22#8IhtZ zvU02kNiuykZNbx7blca2w(C6gFYuTF65{Ugmwq10=TrxE z7xUQ%qwWA0|J)x6*Kl-iVGuS6yU(EihPU^)p3V(H-U-0ve#mIpKYmiBfonL>1n4-+ zG-HSSFP{q*o5~0k`)|VxE2xDY*7L2wrGQH&)mMKhSzRtqPgS4sV_-2#*Cmx^&D9}>zdAb# znl~ZS$3$weYg~0vN^-<#7_Tu59NJ)qtI@$vY3~E3R*>Ffz@)?3@+^kI;XRQ{v=|%p zM&*TwG`OE@b#Ke7^_ZpL&xv^%Db3p}8xz6+(ellHeRW0DkkKi3dNX#40JkkP0o>4( zl(<4i`%c}D3cf6--kJJSK_+L#_KDJ!udraLR!M z1!2Z$QEZmQl#icv9zU3L-k7r->JhX%wERSb-$#`fIJ6X&6gK|l30|)^u zGTjC?GIdWAdv9BP29f9{@L}7Rmk2#pp?7V9EDO97B8LjFWYqG{BSvfbgld!UL)T3c zz7)!XL3pSY+p_LY?14d(e4@cc40>839>#ZG$wmya(5q`AliuEM?Y zJV|YkihOMqIjofvBP(w|hyFL*S+c+y?$AcUoC>(viwlRsuzLY??z%wEukGbvixk}L zzH>!tuw_o*N{m@VO9xW*w4 zBWupvzv@-K{Rk;Vf4ORbcsIZV-cU>Kg%rjc=6{+EJY5YA#9i$S1zhRIzb^=>Je@nB zD`S1K7R&|?`JcR_>dW`2=ttC7B_(H>Mhgn6xTJuiGJGP+YIS`S?%I zN4f_ik5!ZK@VtP18pW>tw3Cw>VKfx=)Iq2}4^$ng7g_)tO7f_VWo854XfoiPBT`U2 z4BTz{hC)-x-WRs0B=FQXYwU`MSw}Ib|ybezj&%^4X6?^ zGc%n?tIQ{mk>2g~n+$Np(kOY!->2JHdpZ-6n5!A%LtqkdvlBYx&`vBm93&aZ(#+d#Q zzUpNs%@G}=o7BGZH?MGzsv#WVsr=T4T#eG07*{ln|G`NC*DnRVC%(!D5o6r+BNq9s z(Kt)S(VZlMa&zY3cWB3#eK^;My69U%Jg&}7aAxLe{d*Re^bxDQMn?M|&2i`rC;zHt z9ug&oMZR5#$!;Bz)GaXx;jJ@0D*NT2rx^HjvqqfzVYBwoaHVeZm$6@oIQcI&hQzsK zG7Nre>h3K;;2J8(i*w-&R60Nr-rn3vNB)0U`>Uun-=%*X4k1`^Z*ix%yS6}q;_hCc z6nA%bE3O4v+}+*Xo#0U11I7O7_j&f-&+lDppS@?v%5`$DT=&d;WM=GHH`IgNENCos z6;&Jr zXcA)X{E-L4Mk*%m9fHlee9^5 z;d){8T0y|OwE=#x0a)5t>)>u>n%3^RTCd62((b!nMXtIXAyfmMkmg&pQ5NK_`Zn(O z&I!VIbUNbby2=Q+$4DtBVcK7am*uYuKL`{`k#Mu-YvaFrreToi`YJ=o{00@<)5bDA zMHmE_4Kzi{(UdnXMNz_LRGWAZD)eiMdWQ8-1fq-2YZ7nfi!H-ha1xUy)*31QCTKiC;fibx?|TpgWkTl)Ez$joUu=JEA$ zKjrtyxg3o#JG;ZbE-)$Ie$C*+uf$rAT%TWdY7<3IqAB6f4Q*% z-GrXWY7@!CjlUjUO6qx1{jGlFb6Wg?z)Tk$m(0af#*erjX~ZBEjH&0a!N6F^Y%E3ewOTqi0m}S8}X>$al zuRTGEKUez2@4GZaBRPczKQ~k(3tbHbDYrxphdkQvyd4JW-0kt-K>%UTNIhwVW21*H zsfvbm_Gs6gQKXC4W26t0Xgmq8dlTp0 z2PM}P_kSc8gl@m{j%#+bV4f?{`s${-u@pH%yD!D47Q(uc|FQLm2WGx6wGn&D!eD`t zPLqV_ zRftWnv+ZNn8{BU1H5UXInSc9qRstq&v zu+ry$n#`rEl05&Pv6KH7oMXW>IW8xRYSps^0sSYaMJFcoG^okCeul0)jgH%Iep5{O zvaO>5VaybCcO)V-Ue7amMuCYz_jyeJm2t~PTmPSc~yk#q! z^yOlS$%DNttXYa93JikLQkQyBD}0MgS;TWDAVp^EkYB{C9O{o$KZ+()*Rs>Fs_(K= zJ?Q1M-DJO`1EQ!NfkT)heh0eP{j@~bE7brMScu~cUO_=QKq`^9mD2rnr|PR;wR8$` zH=EBti*}A3Ht#gW{}25@g;C}4DZ@55b=ZHAr&`$#>Uzr=p0B@i;s(CVT?@M1R6FND zH-8rQzrQ|hWK>Y{gDq_FG96Nd3_|C(Z9pDjH-*w|h-O>@nCLm$7uHUfJbQ}djjhX#-m_)|JiwwplP(8tCa{0jPegXXYg0FNE zW1p1+>`Eg-^b#NkM>r64hm1*VFSQDx%U*yu=p4sJI9(2`aP*>6XTQQl>mz zWq?nrzwaQnH$VdEp|#vz;`$IeI{*GfrcC4hl&G9kRwO_FMvNYZCb?+jV1NW8Zche# z&^vHaPT!O4B)ViyaAl>jiMIIFpv7b3Bg?E6yf7G*=Lkrfd%|J7JB%5kaP}M+3XtdjCK$ zb{}*TqJWg$L^)x}-cS@OKXVX5nEeh;SN%2r0UvOfFVUL_@FP}A8B*I2X56UTy2wc< z%^`hZVQ4gq2wbYK_Y|g`$#m4@e4X)t2yGZNe8a`hikJ?7lOEUMLx)iVq@JqH^#Zz_ zU-lk!Y+sK0x&vcnItRZX!$d0FNpK>Gx1IbtcH3_9qp z^0LL^BErhyp~L{;P(2T=Cc58s2xJ?BlYg*`40vKnwW*w{m(2-fLExbzwf4_+cm)zY zvA~{YmnR4NSjnDBvYuf+-X=bwG;VFi8$^t3^W!>+kL?Ocoo1^t!woDl6P=t`1DN_y z4=u@ii-d*&ZO$7uz|{-9c+;o5(#8}4bqa!x2tdsr>}|~)k&BN=Cfp;PPN#F#FZHeG zSDL`pQ(TzUgFJ~|(sVxfdvqi#cYe5Qmdme)V``SZZu{REDnp2ws<EOZ>mZli;Nd6c!U1Q)~s*2LoergL;heQ{NDQKx_ zeCNoXmy8Ty`8tG<(LZ(}U5UjS)Nmd{Fw>Gi&yIHXl!mcngFcg2DJn`vhJo=vSI++x zf{6iKeT7WMr~dfo2i7ZpAUyOU{+~mC@8y5K)_y|i#K$At%GomzDaUV)dk= zjL`XeDBjjb0oc_)mQg*~jq`!6xDW|*JtT}#-^M`UCf2c{^YS9aY;1E)Nds_*4v;OX zk9=2~>^KT{&>NH}-{w`gvH_XOu=T*o;`iyuO)hIJF0C%7s@lN97D!8RNHUFPfevEUz`e9F3U0-s0r1 zan?rMQfCRF_1`r|P~gFrGnOu5_-P0uU_!nvzD!z$`25LrwY^fF_C$jiv5J!3z@)ib zg8V*xf-qXo8jvu&+txer*469qxN1&j)#f!gnPz2bB5g-*w79DL5L6p(aIgwoj$M+W8Z2cC1k zR~dyTx7tx){WfcvyZ759u@!A^D_~0rx(mh$x=#mmHSgQffr~;MU?P;u3^s{cZYf4&hjJBJq<3n1ogH1eeK3l^3*-80kcUrC+Zev%z(#-ohe zgyYMN8J}%5e4)lf0*fIQ9^B|rYN*0}#(cM*;Yxw$k%q`;aA8uyj%($icxuaMt+!{e zEz@xVxXs1@UU-`Br};&mnV1l zL*F4by|U&Fvi}^o@cs2zjQgVHp22?m#hNYGdQ0_G68KJ-v&xEbrhc;5xnua~3m6%- zxt9>LAWUs({M54<50x0!@8rUwQ~nhYhtqhzxyFWxr1v3A7txx*qo{CAs7Vli*Nroi z*{T06_QH2c&RL2dNPhQr3rj(P?fwA#2#1J-YZh(JF_W~1bZcM(@KNMk0~)QJL7Wl0 z%})Lkf8#xWJiuU-Ph+piAAh9Nsu6KXxKo_!sqMMr&x+@Hr^5hL4-rl!l(=SDBPWUW zvUxKf{nBZ>W;eGHN;J^F&Yzs&J53Q!31hJ^Qg3M_iELG`_?7g6?A6<_@Fw7rxDsm! zht#>XaBPhgf9%cD;Lsl;qmISOJ758VSXA{apCc6QjCot zUkT0QBd)Al>7i$+Ow9}4It49~J`LTHp`q4m-hU-yb>!t;ECr!erz82F-jOlct115xpe8R}3K67SRa&qRar58vPOp9yrcMp`k|1!v1 z855dYVpET*s%l6GEgj4UCpgWjrgZjBWA$2T^yFw1C|fZw;<7*XoFn>4~(s*0tAq4?w@_~YPh*GUm?9B`eZ4PQ#$VTu}FgzBXO2@$88OVJnTVKmXIc*Y1n;@bRH)>j4gZL#6$n8`=aM`abXwg zL1t}}1jX{`KFUJ|vl^(x^X7he{-brYZ1u4@V+5t2gucSw4}l&`9xP^Vs=P%q~@&Q{6rGL$qmrQJ%b zLnB>Sq72KlRfBBr4&SrpxEueqP2Uz#yB+n*woH3OIt^wDUJPh5#kRA&!toJ}F~FHR zXbmg+Y!4>Omm&4bYng`9;Bp~m?59WaLsG|S*bE(($O{>~rF4i#$3eAYJZ|akm(Szm z$XB*7tum}X_<5GXQzKE!hMxm^wnijM)xBOCk8R(aAtEG|^`L`POsQFW&VVGb;sX>j zj=^_oF`|ohCXK!!0BT%}_YD$bLLy+NdiHe6DJdtl zjDKL9IB#0FvKGFgFPY_NoTsvVF@3lx{vq#qKHeN|-Ay4H=dp4{X*$U#qxm(0hZvqA z-Ip!0^`cb?ACS4N2BdV)04%`*7JkvI2swhXC2uy6A8Q%v zLJK^drVcXB_fp((lN^=dDBnew@X!02-3`|zA*%6AG^&S+b8;pCW*_vN@Z$ST;%DQB zniMb(L+`&6Oqe9$BxB5Mg!>IVd?&-t7{#aFGpPHT8abO~f_CSxq4bbdQ#NHBFW`Up zrq;3gQKj2t36eh_=U6E$v9}%d^OL^+X~*e`2rYv@hYaylvA4R>=*|A7C*Hv~C@iqE zWg;-M|4ClLd55|64w~5@jg0nY z4|;8pYz8l_e(H;znR&R{j8DuMD8t0P>P3b>b(Pd~~pkVv)c9q*viWlOTI@ zKK=C&1+;5vUkXw_ril3yWl1_W}A^lhC6Y za;QK6=*g8x1!Nud)S!WM1!nw=Ci5k8KK{_lmh4_?IB=PtDu4Qn|L|!|1r-DyPEsp* z_Ckin`P~0M#Q6{r0OD#84kA7b40NmwUnLfi$k!3h(kr)(dV4OHah==Z-ya*7ohnw% zlGG{ab@He6mDA(8F2(47LY&B#oV;;bXJJ|a6kQdlpxnych>{L8@st4Isv9RK5ljvn zn*2qyuGV}hQB9Qhm6@6c%~m}q*A;W~CkkeaT^tWamB1JdwHtwojKFerq9gzyOqMGm z7aCHYfChy@NpQ9gc^D5C!`~mzpEnO*p3~y+QV<_o$R6x{jsoC~$6%WgthDvottPG~ zodt^=3%}t7U@F?qvh7)Fg>Dp;FowAhG7gw-StTRe1`lHULZ(B#VZVJtiBA%Tf4Bqy z+`y$pvZ^k`&>BL%IR&~>&o7&RCX2=-GN2{qS{Aq)tS@1DfHrsw1VlnK4O8vWrj38} z`d^4;gD73QLSBs1;?TR{L(3_{c zFST(#toq!ork0JLDsI-aKeVC0RDZnhFfhJ2*u(qyI76f<7b@5Biyy1I6w5eZc<4l5 zuTo47Ta8OM&{_$rd-e!FHpHN}?(Hlhh=7{m!y@zMrPTd-80!j#uaL39j59)MS0DQLo#BSef)GT`8+$$&1W*|s>%4lZh< z)db}3IkRv3S1-3fM1!cGbWPrdF`9OAmwM!<4TRk8g}!?q^z1(b29WINz8W+8FnmDQ znW2sIo@E^-IWHz^=l%Ffec>r00&d`vmewU7nG@F=E~_O@00PPoBGbtiJrKW=dVuXN zu!EXtW0vwAWXLAO4%l*f->#N(j#^cwA*ve(36+{YNBEfvAP>!!_DN037F5zRm@DShA)v=muZY&$j9?r77O`R0h3 zyP)sIRCph*q&rYr0$xEVK2mJJXk`{1Fo69n!BBEVVX-ICs(8ye@>xXnZXUt)4u~i1UH4V3BNN5eH^&{=H zRI64go~(4pW@JE#lFmUz%*L`GiZbb#ye81vv;yJ3xuH9nm$0F!_@}~e=3;o=RAn5$ z^&WLucg(@9DO6^1@Es9oXrQWNDIf!djW=-Rx!1(NDybdoDtyxs8+;nYUX=S&wm4i! zWUNNXREd#P8kMPEMdWWSNYD`t-m^Yp3vfat>GfFiNq3@ z>@%lH<(3D61-Onu`V`%ER(x48V6{75<%&kamT z{{qwy4JVxcaEt#>jo;OY3qxM@+}OhakN@9}_#!|H9W-A=81|106fGFkCkS77lL`E# zXo(&cZaVRG;_;$o5pqqMh{;he9s+aljuX#4U6jbteJrqs8uAJjhlIX$(7U#+`6Q2q zKJW?5s}r7))3tNQhVfK(xtt-h2rky@Xu#$uXY#x$@5caAcp)J^0zO13wjh2m2ck^w z5LM<0$IpV3x)#*ga;m@hpHUC+28lsYod7&u>(dvzVwN4B`QMLqVW&2&IF*@m}#DN^k~k zrmI$|o-1KGjmos~vdVlR4to|}cqW2W>|2`>vRfk~`@5I%uy;_IR5ogX8DY238EIbtOD5yE(t?htGo1*9|L++dFsWe_263pnx4WU{qy9$y_e zlJSt%;lfg``Qj1LX>)mCj10(9_fd=9g*xz0H4^5==pcl(-i1alna$<1UfKD&F%t4&7^J|2Wya27@?fnTs@-a-nf%D! zH7+dC&Wvw6OCEJj>}Bx$847J331#Y#{W#h``(-t_YW7k7AS57isceL&@>}$d@KCOX z{Te^j@AtLoJ>`~t!Ldvk*c-GKZzGt9NG5Y~>=F}wQIC{+D*q)}MlO)>aq^ZQMBMiW zZfhzpUP12TIhNR6x(nyySZR^&ML}7WRNLKzf$+n9&wb~MTE;`vrdyn$Yin0BfP_TQ zodA1*I1oZk?rkKPgNeo*JZG@*QJnVQ{O1!oIjdz~wJ&TzVP-Pt*DFqqr)Y6T!OSI# zS|@VD{N8WVQ#hk~6Hm%KAfA^^NdcfssQIOVauQvC@M^Y+&(4K@`)2>DjX$G5(-z2< z?BeilLL*Wf9%jZ9K-P?bhd0*wHU6UX7xvM&=Dd;t`i^9K=iN1bgg+X$hz>BBWm0_~ zC!sy<%s}Tc_XE<`EqsHEYlq1-mm?Si+1K4>^O$&s(wT!X9BJIHZZ^{DLFU%4m238` zjkcoUBic2wdrL+AA0=UoR+e!siWW(|z|;dv&3dgF>g9SFr|g+$V;@_PE;Ze#5-x3l z1>`mZ@FI`Yr{sNqB~%@8}Q*bSt>n)KPKvVlqyJ7;{m_+E%AXQwabOP+OL+@rb1p4?Z}WA z7}1U#0u$?4!u#Z}PAuq$ZFlWf2s-4nDniu_F8QmdNwYR!ZqJj;Wrfb?hKyVHl)KaI z)RM~Q{lc6~2Tf|?0cLF;ftBDN)CUIJ)faECSGX0t^N(jNe4r176X}knI=R@K7MQ2i zdPitCyg#0?+`pQwz|8mBlNc`9TS?)U)VZ`|2S#>-^`ED1dmH!tUtR#1G-fHgc&#y3 zOB9_S0R;tOEGfZnYS402GW|Ol)ImM=f5PqV$rAh`m$|AVPplVlfLmmwd4X5<1Q#Dl=78U^_@hQQD7S+#?6qT^~7tc;>WnK;a=TjqI4=3+t?7f~^BwJj3 z=aYfrL9a=bkA^zTJ$X6*hot|9#Q9gee`!Z-`U5-O>H5al>EBoR--^?}Pt78Z5%8U0 z0t6@15rDe6uy5x0bqrC73;pvd>M^w1UJ|z<^-ml>?EIL{5}`w=UV4Wd%qgK zZYPKEcA}rJqH(L~#HMR8_5=4?o7ENxt~0JI%4*=_b=%VVg5A2)xl@&A<=MK40i%{N z?OiuX>)A(@vsUzZ*w*Y*R1i`4djEvpuMyvLoHd=wP_Z$`-2tw`rHkl}Mk6b*EyNx7 zc19T|Sh4-y8>~0;^sGMjr z1o|;hP+|&b27_U{KF9|cB}Q?Jc%M5uxzmMjTHxEIA5+cBxqXgzZsg4R<`_J564h5= z9cYynCeAL;5}C~6*$K8V(XmDE=73i6q88Q9Iii{-w9aPF%oJmBo}idv>h~Eo8c_K? zR|4&>T(_Bdy13U0@a4z83*RHJQSx8X&u}0s&97&}OyIVAA`3g1l{JEdj+~i+``gGe_IbpONi;p96R9r2tl@ zIN?>y2|(bgoJ{hGzLvq@?tc5Sl8!nM)-}jdx%e3S6z+dCSW&VIa70%b=3op0RUyR( zXq>MrgIErlv)~PqF#*L{$TrSz5oX|ON1)_99N>L(m{Nz>YTqaIL{vm&TP>vy7q;~_ z;Oc>GGhMaiXmSnTz>dCz&|<$ETm}^sH1bP_EaAe?3`cm##Is%R8$g5QT}bkH26Te9 zoi6LJLs5M*nj>cGR*eX+5aewGBxU^l?|QR%KFLtkiB8#$bA3c2r%kYz&-^s*GPlph z)2PX!d{Ffunt_gFk#@g#LcEkHg13)HPPc@9=QqA!yx{cfcgMaD41!w?t1Dxv0xXTs zm5BqfohI8_J2N}~tAlbE(XD)$Ru&jFiUSjK+3x9rP_A0@HITH=5(RZPZXI6s3Ry&a zJQqi@cn=odVeG@gAa}1=g?j!l=_s~vz8%m~5Zur`_1J{}eiJA?=sD(U#KSxRA$ zWRwhB7vGb=>g7)+pxGKBH+^W6lv^v+C?-rRjcsBx+o0PxTt~@`AHfj;_Y?MicyTTz zzr259e~!RJw@th#f753rUQ#6ot7xVprue@8V%`Qz#vSkWnr+97*7vmcJ_vCEdre8& zk?zMYp0-U7m(WRREe@t;-nh89qbkKX2PctuybPd-zg)W_OR%*~>!*0049ag75BM&h z;9LU6GoPCEsqMldu@iY-9Yj|fOeki52?vXQ+G3>yR~b1P5V7X!1u6X^#~TD_#ojI#_U(Ef5a%pMuD^AQP;-T7g9dF^((oQ}mjrBr-I!CG@$pfT z80a69mHTFXyM;^LyW@e(J8E&8oH#^02_MhfPp?mLdkDp#Dvqk?+w2Y52Vd~o8>{F} zbSefLdACH3`oCcHXN6kbf2;4IZUatGP*($Io4DnF%L`8m6mbkya6#Acxuh^rHF&zR zhNTj{vPWMeWoR(0Qar=xF#hy8Ilxi~_TnQ7cII~}K(ucowg1+xb z17M{BQ#am(`P1Tej`vM-YYBa9t9_g`dYnu zC?@Eyj2Lt&^@Wi_NK!o9$7DblOS@xDdehCZx9a4LcG>o$KcI`k%H?dRuS;L()#KrA z)%DwQ+k+Lr0|PKypi+>(-jt5BOgji8O36NZdyoyQk;UeMt!_NEDs-5hn~V~Qs|Un` z{o^-kk`X!)y6pfR&lAZ2S)v4i67U*a+8 zzaM^b;sWsUp+hXlcztbLJ;?oniQ!4uY}QGAtD0IE5t76?t(wbdoO@neLf13F{Xc7x z%jz?Ko*WF*OpS-FTjxAymdL`5IN#Z?p#P-+u?`&pU0A$N-{-n*UDutDQgn$*rk$V4 zV8HaE9&^X&@?ny_Ct)Ly@xM@gT^~q8x@hF{WWU^_Mwt9ggY?*S+K!f-NWEk>a!=D5 z=V8+v`y+Ex73NgF$McJt%^7jCHCFY-dx}6St5?D)$r=&6U4;CYJ#ePc>K`(&EtE-5q7glqL0POHx;=up5!2*W-V`&9`KOj#`)y)XT8@>YxnpJu=Mi$od@i+oH~nMT2}TJ zRn(j?IL@i4Sbd%}qI`Yd%&Il!5H$x&uv!y5=A42D-2p1f1@`WJ??kOp%{zzgXCo0G z{VTV>=N#CLv5GH$@M~%gdEbDbc?S#a4jy>&MaLo4rk!!c#>i=+WB)UWLS)E8yIV%8 zlK{wXq5DC&PN#0 zpYluOZ*%NF04W6f@e$ujKoTOweA-FyQsLM}uqv*2wHX~~F-#L$lLKApt~rq#E?b{q z?mqhMbM?d3X{TH~F5a(z)u-&3?0%>QdGkUNN@VA4Y|-d3kqzX&XJv38ADF&rZ|)Ds zYD4-h^U~gU`+)4ESZ@5*W^ly1P~xL2%=X?BAF%B`!x8rfJ9!fxpiV@;%V$L)d;l*1 zgBkK?-g6`?A%+cg_x;)4)^(pRpXa|c0e+=54Lsn zb`-o87vC)^1z?O^e=y8TbX&9$0kwvilY-?J7NiQ)Kc-uTc{E5UOL~jecba{O$JR^T zT=pugA^vbWSl`DjD@FbtO=7A!?`aEgfxfKrj**OigrQ3BVMBmp<*ZpFeeJW8FxdtO zaDs6Wub~ucSKEzxNhX~G2>61fh36)it#NdGj6VK*G6rAvPDka7E)51Eg&wV^4!maQrXJnW5jUvr& zPOGLjtC@jTRFgT0{5Mrd<3x+AoA3F6fQ<&UA_+>;w6jDJ>?tn9`@Bm%q`#rs;SEK5 zo6WPwp@`YZqNVTiEx^(%hp})w9Mnpry9V8ukcV7v@SfY@Ell+F;oDK%w<+xU)eTy2 zGon!u7U*paPFA?I$~teo1@TA%K`aGE;x0sc-irx(^j;Aja`E=MNLFdzQB@}k>h|~D zsKf73x{ZX!xQGLo5ppZ#w%8Rs!gTDLv$`aM3TuurF|r4@g=Rk3i*1jGhEl)iH@lpE z*uxOSp1!CD*>PZ4_q;h|az$sJY=QJsKwzf(CDuIjFa_lh&}c@lyxF{w$c0!Q1I?S( zc>o~-LkwuAk5oNdzVRTh%AM;`bZbMf~x zTWytEW>&D%PDuXkZcc{DYMH>7x z+dfaWe}K$HbcTp^QD1{Cs?BK2K!5o!Y1-YB_K;P?Bxdi8hP9^ITCjCzGJ_If4H2-;;8c zc85%P_j%y&PKrC72CZi%n6$l^XjR%jW7zVMxa_1DoGGhqX?=7YMngDNLO-BiXujKq1z-6m?A;a6f!Iy&v-OQgjv%aPk7zTt@1_GbewumC7y z4_>X;EWKz#yBImjz3%K)kyU_0y-F;>0cw&@98le-nldt*N07txhz;AO-8sQ~Lax-? zX|1dG@-C7&b^gzkf8cslxr7)8>Y|3d_(J?GYXv8x5=tqehv93T-mhJ> zrc9o{c4!)`Lfo%e>I}QlSH-(N**LcGV0hI8RddLpY7Di!>2PV%gu#~-m@Ne5Fqku* zVMC=U3BSaeKa?n!f94@0#Ir526}qhrEq_D(PWsto@*#hFy;YB*j)BHQ%79z4^BM6q zi(|GYp2scgw^Zw|L-EBMqK5T+ou^n!W_z160Yk@MB=!R0so@TY{f?T+x^05o0w+l~ zpOC9`pvwi{)dK?x+hLNBEs}^DGX19;quP?E6(WPoVS)I2LpTRg2lolra)TC&zNABX zL3pT+-SaY0k8Ir@qGC^NFa>|oPsiU;<^6~U4fXSJbh~3iZDA&OG_**0`|_6Ti!LSNp&-=>0^*=KUX;^SZsRYWl5HE; zA7TX>Uwkj-PGRVvER02qGkdFP$Qq@cEFCn+$Be6uz*xk~q4bT**k_J1RCzmX&nvjR zb3hSDu+sSYSK0Z3(YK_f0z6U(GJ!G7I+KPgZC!j(-2}v3Ted%*C8j9&dR8giaT~GI zVpZF<)FOA=>?^ulk^yy@7>~#itvn z*{u)1>WNqCPSJA<`%3`=vR~RCMuS;4ZhMdm%Qb7wN`vl4)OIEWEpxc7MJq~(m>%aD zd*pv$OUtA*$QC5DrSQXaJ=Tb@20`dr2Pi0zxgGnhVvgYeG*j$IwoIgx`!P*QlDv`> zu#}`^y1TcfXjCTywVZ1*{$_vx+O*H>)t2YNE4dxs69Of+;B}9aM)0CcGUKWDU6yMy zRoPxXD<2AV*h{_r4s+$0o9{^0r10aUw?G{!Bo^v@c{1t!WJ%E$Qwi|XV-@Clb(P`6 zmpEQ`6*)feec=#GK3VvQEa-bYF7iIB#i^06%E8AmHe`p$I>F`nS0o&1!a+@eFYCp1 zpoX5ae2U8{Kq$L5?rYwMJgU?q)S?sCL_+x>c?}u2t%Z!Opl*1{CFX6DkJaSq9F&Vn zCX-NKi1*7`>zTttz>={T#$uUtqUk}V-P6k;k%N@PB6C*7QnC4Y>$>dbdMN3}q{`NV zL-%(7c%cuw=WXoq!fB2DwVBw{;iVh*+#;c|6dpIbdpab0EB%B<7GYkC@o)NJ%5;0c zEv{7ewZy-`b$U}R4z}9%Y50xmRnhd67NcZGEfEj^+#?R*gL1 z{D;Pg2!7zRos8h78Cb{K>Tg!7^}(0Hw++qJZrvMwclMFfx+=Pae9;bl(080@0$BG*++t0_>7IUTHG62ugy#{2d~m^m=TSt{s2RO#F;$}G zU_1-&qu$Uth|jRU=i+APfj8+s;4A>m0HcnLYd28ZpZi#!N<{gFmv5qI^oxRNB@k}h zVbC#?6xpg$;9w2O8Uc+nyL@eTOqO<7Q!J)Hj{H(pCC9dB-|fjLG&m{CH_dGz4Ld~N z?FXC2n@|R9EN>J)T|3*fGHOrb^Nr~2-`6l1VZr)l@gyud2}QPQv4#a(;7rorO**5= zTM!c8o9$dG@eQUW!(Y+tCMy0!?(+)s`JeqY#tO1bW3 z+u{*b$f>i|?xKypZREZjddprLlH`pVEYqFM{Lu!@#*qRHT^1?jm#y0<;|vbWa3UEg zm6x?9OnhdoH#rjdRb&lr$0?)9)L}#PcB-7OdHo+1faJ)JS9t8(+_r5Z$7Qp$4o4K& z^5=gO)_6MSLw+a0QnRmOaIfcW0Q2ceKZEGe&POR-mzSSErjlkKzfC{ML>?Vg>)kCM zq)%P#4G5?_E|mK|yd30c>5d+RRrab@Uz_+$J_qK<2tMu0=``Fu{8>HU3^nlO>PRRp zX`o8@G*JZwZVUa9Ix-#+l{rGZuMSs98r?NXr4d=A8nfIRr^c9gujX&)F}j$9CB|0( z`-V_|X~<)BV+8oM9Q@swFU#wik*ccxj4rAe*L2UIiQ`q?$XjA;jIXxq4^)L2dJGRv z@g}$nm{An8_5~Q4R<8e^8|qAL4xNlWTV&yWc<}^(=#$@nOZztEp(l78TP?)TQv~p& z36&9YGj+e0K8;4W{)*AR-I=t7(t`LVubMEC$xO8i(52;gJi8zGDwBC$jk#hZ#cavQ zz!3GJVO0fYS)(&p=CC;C!!+6Xl{e~56P;~7kaeaI)I;AuD(NSkZn!2Ct~x5E$r6$e zj}H#@BB#+P=7v-5(GL62HURxj{<^tt&8qaT8&)4yetPPBKi0M)r7XNyQGnvnMq@a| z4Uw}Z?t}t!Bs0@8EAGwmMsC7~#hAx7bVSLw67K}3TA@aYb2*CtZI=0$bn$<_-t$nB zff`=T{Q58cTf)7!6*XKKGKz&Y{6rLicyJ{Cj4?$PVn?<~Cn=wuPF*+b@*`jC>WEA1 zGY*SscfJILnC#5E1IA>igw}w=Za`ip_kk)A6U7zCfM{PfNy?hZq!&c#&+ewU2=;yQ ze)d0JD1Th&%V~^^ zg8TCa7Xvy17l<;zZM3u~-&(w+UYb}nBj0BFvJBqW`%Y_AP2{z1cWY4}{8)}IY>1%S zT2(W}D=X|t03d<&BV8kjWAm{a(J?S^ddzA=h~lkMDO9bj&_nk%TY;+94t2H|Lk8v% zb=O$a30FWXNyLYdb*kg){iQuI0u*xt=V zs5!9zOy08Dle);fT%u$^SXgjj>+1=6E`^108QFWEwwZ<)3aQPs!`zh6QhLz6CLw|a z$Obi-oZMlh+us!_k%E2q_FQEWRKtU4hoA_$mAxuhSk{KSB}pc; z4k?#`(becWEg-n(6y`I&tu1QkqZAaajiYO(z;^%qMfPM=nWw+$l6B|uBs<`VH2XLh z!B1T9G9PgjeA_)1-zy^O7vH1y3h{rvx1NSuv>ZsQgWGBNG^*7PG8yIDRGRFy4A_j(th=FaS% z-Jf$yf7Haje{`Jx-f>?t|AaXm3GZCb?rIn|R6g}W`kjecO93+CX>Sje3+ywWTH2fM z@2i|#mdeZZ?k;0t~ z&!kNNYhv3~=6HF{h?IBV0Dca1An$ws<3%kgNe9F54NO&{^$!XWYTN2EOMnsl5pR5) z8~VN9A(k!KAyvgB|3q>yGAx7dpRLG81CO5$N3^89znFB^CnysMhC-nKbu2>N5 zB*pX-!F$)irBBF!%vC$^y-v)1&6K-CmhUj0Ik2khZi-{zx#?w61j20Rvv!7A6<#sx zeTAR1V~c!XKoIHdEJ|>6i_l!8QoO}j<^7^{G~ce1FR0A-fN#=lamiqi_-W$n`uTC^ z66BCv1-p1IZEx2g=4cgvfJDH`A~B*QJ)gOWEdIs%H|r{+YSqU()e^K|!iar?Z>_GK z&j(+;|*A0CafVZ6n2X<``?%RHIz*p^Yt>n?$i=ZDS+8KF3*o&<8$@&23va z%ABsdI3Dm?UO&Z2n0H=0Ei4gK6V^mz`uLw-w*LUCf3$7?S7gK{>Aoj})DYgF|3;UA zfKX`V0M{VuhDKIx(jUIi7PdLpt@5kg`1y5w6z@z)NuxVyXqMTDko$HJ2DZ*&(MX*- z$2@KV;Pd(Cjp1y1y7OL)`MKn1iiU-B@Kd_|zq|m}6?9=^UcurTBmSFu)I`1UqFP&* zUf)Y9&C6cYdNj4ZUHSX|c#e4WrS!>N{N0 z&I_M=)Ec+zbTe~Ocpf?NmVDC_#CjJgH3{|aXrV_5Z(WlvmYYa^@T`g?Z+&;*IJ7(R zV7t3LKX~4Gly9D6Hlvg5RBKF9;4NE6ZS3ZGL z`5Y&)!m^q}jq+$Cjh5qil&sLAF2Uv#{2OPvGkwQ;%@VUQY{ZzFUtM!#ei$rlO^7@c zh@gK3Z{<4LwBH^GUcb-S90soLAP^@V3#Ta<#yUk?SGRr!xnE(3S72n|vEJL^fdHcLVsDZuB*T>_p?0P!q9goQt z=pswIxf5B#{!@AhT?>r*T@{q&%k#Te;frYQZu_z>YWq9Mlx5@E+spZq-NA)f9tSXD zF-Dla)$heGzThuO66nTq5-Jf39+)5fSAv zh6WXQ_1@^U+_R747WTtLfGm6%C&e#&&8fl4j2#eDEr85FE*=$jv0m+UL$y;06n;9- zYH|Ne$-}|`HpQc*hG>=un^MMtQ)&zhBa8M|O9aTdlo*A`*XfgE_nnZ;GmZrBGX5&n zLWH&_Hb#3oNmA_P^AA!gNIc&AS+|6FJM#2b02`7i3dtDW%xqv7FQ!N^MTgLw>$x2~ zL<*`yKb8T62mVwH^UfM=DGoE`hwQ9LaS?*sX(6;%PAxa&0=N8C^IJ^{fNaPGuU2h% zz3_^kO7Skq<`qH4MJCJjnngn&(8?(lU4w%TF`Yb zlEzo~kIns8zTBAnQ*5{?=v3xA!8o)kaq9*T>ZXeV1{GpTdxSMESce z=QRcZMPYuicNFrR-*`2xHEJxnO?cTlu;M0R)=UXW@TLOZWpmWYl&&HnmeBs!OktqY z&A!+H6-Yk@0z3HbgPP|Xj-uw$M5+L9|cPBoO z>oa>FXpKw13FL$HdTFYVJ!-yD6(m?99!8hDuU`2kw)m$q+ohMthe4T;?r?;BHUy3U z2PGlrd*h;p*4oE#{VYH@D-TfLRe$>b&v^gu+VYvW2!d@B+Lvj)|Lyz-V01LCgAe|T*~v2dpm&KyT-u76&X5Uw zXvN6VnRTM{%yGR{3E1%H-(xwSE7$SvIEQO9rtq3EY&WOu}m@IU6_Y z*56V!1^0PR36PpaBqUH!MTq zB;_GDXjd|aniA~;%-URV+FSL!1oqc-^ieyfTg@9EKHpZM6prTWQ6$JA4m;xQzB8IK zXc#NhEGZ(E9`LzxRt`7gDm?p2V_-)8I&tms-;4iau5&)qpjP~pP}R9oft`g^&xK%# z!i>n+>=y0G=}QQ$|28m|Zj86ILZl9@pva`;N|QU` zrtglqR9lA@U{52%ztz1fPhH2~!g0m7SrBR)uM?J@p8)&+hypTF2>zkIs}8;wSlRht7N7 z@tSZ7(){J)w7AV-=88!ekyID$h@=<6+W{LO>0HsMgHxD;sBu99zq}+pkcG$EM#6egP}~cwov~+M@8LCW+rZULuy)$A$=x zM4!)4Va0y|7<}m86bqtEu_l~8Y%CC6`B!~0rWqy#jT7;`s=*o;vs1YCiBl5a!xw(U z$N+qV!G@(o83+=b{miozLi;==TZ`%2vBq}x1}RGAQVpyMM~(2` zN@}>-0j=-8N|6UK$)44XUbHNxL2@2}XCuH%wo^C>gu)^+HN0g1BzEZ_T9I>wJw=W=kF{z-^4-R{kM}(|K$a$4#yv-6S?k0CJ>Y^=G z<^-$SDnY0{8-RCx!Tzh5Vkr~qX*SOnt)Xtnfy~F#WaqHw1!m-Cx$%MgscEQZ0s1Zb zKFnQ@QN6qn4_niSiD17Vz;XZ!v3V!mvT5z<)FsOUWHS8)UHkD~9Bq#K>gmyeuwzfe z*gB^hk`F7TXE2gon0Lx4S$oW?La}pdMm`+4Fi*Wyiwcc}788e!6rnJu8hYv^P7Sc8 zU3mW5`@|pBu(ikBrCCBIBoQmpoa}^EGsGp~YNME21c~E%Kqmhe-YYAsARW&bhBO06 z{OH_WfQKf?uIh#*i;~A?;DKT10tPj}$Wfr(>VG$*MbZ%vg6tq2O#A=fh5s#9`e-C( zydS=$L{6{^Pk~_74!iUdpg3HPwN)?b@YN@Mf9$W!>6|Sp`mIT@ypT*cb>y~$s}o#; zPD)lk^$U!skPPRqY&v~6_h}i33<67zi)S7TFnplwzev1WpZ2E;=)tsmQP@gap&!wN|bcdN>p zN?F9bBw6Q*v7M3=i`9!&h?7#M1-{%dRE#O3r}vlrqpC3REg{bh?GV9w3pYyPhR=H! z=XIGiOb~jtm@p(bO@MdtnyZ(5kJPhL`tD4?_AXFdV2xvSp0=z-ij zN=2<)bQHs0)6Eo>@7FAZ}AFb)VlQv=6bCzg*Zo$6)|cxh42q?{6fTgun|UIf!N<(`kehvd z&L9X8aOxhW>5e(besCV$fWPr$;fdVtAr6<3>m$F&IM_aj`TOvN!k8VHVt)guhL(%z;W8$gR+l!I3^8nPe|3fkjtH zN$Em(*75OmjthST6pWxf1}siSzf$%F*T({s7RAupNIJe!#|+ zY-f3a5%&tP$m#%K^2@-K;`GzTZc>kJQ|7w{!Dqn1jsg-cEUA#)&IV@-OmIW0qX%e60z?czlpYEanI%pL*z$!1-t$$8&qM5qZgIINOFxlGzX{QeFuAJ(J!Z>bTrt@;%k zjYc&#a+oXE{o34)UiW5F%6Rd%uwR95-eZS=ecx*CMed>?Ez3;W3{U{%;{>rHoXyP- zx7mtO>>%~jgGeqLV?$nOaOJD~qGI;uwCtk&(^D>x)PH6#t?qXv&g*+`3yu#qq{%(q z=CQtF6wq;%$z{W9H2jp)JMM8)d&2?~%#{h$Z#-Se^?!-!KPQ3|95556B*eyExzVTa zgsGNRzrGhO{n;yk`y}etTC>_-_U+w#=J85PQP5_Mo5nrZdLwY8e`_S7{V6SMZtx8y ztSl={hX3O!J=pYawqQ#hYLkRK*mA>P!0WxD>3}~-?<*gu>!DBKz5g}%LC&~p-JS4i z^aALD3RK~=QAL?t_vQE~R0vWrq#6o6x(@yJE?HT6oF+rfXPJhWyZEA~Kg%1}?NiqI z8E;4u3KA6KZ?Jn&upP;yQTL{Qd?4SU&kC1l)qxXAIF#c7GaPiFR0! z{X#VEFheY{bgb6WvTQ{1wF?k1%kzo;Do-a@!i?1uGEoT(Wv@x5~PG&E#Ylwc~mqt9P{? zm7s4a38Gwzkkk+SfDs{?0xYCglbikV9R(vlbE+UeNQ4&|oib^TpGz-L^2Ur9zMyo0 zucE0NC9uZK*__(9_VwI@bnJAx*6XGN%8CTae`2BnP>q2Aqe!_XWy2Zb4Ow&Ih>mU- zjD%oHOfK6MGPjSwDyykZbM0PZ-n8bBi%MGgXdjcYTJ6&2KKIw4ZPj8Tb8ve`pZ)iI z7GfjgNAsY@q^VRyXN4|B9b#MjI_(Ka@=pK<7qzxJ6Vc=J_FFhlgZ7~;nIJ<-DfsN( zGS};AozI0uuXSdkd+cMcsWq+{uFJjcuGMOJtM}cjmf?4x!^=I+zLmT=lTK8JkHA&} zq)e<}e4$xk1@v1ny1V~@rm5vEc>=Gp^GNNj(T9!PFH<&i=W> zf~;0{S(*9`v~n9VQRs6;>QstmokU4`cJ4(NfmCX90{a%xk`U6_E%3T=DcNfe1*Y3S zwPn3PnRLD`{(<5(7TMQdz3^)nURGN!gRN@u9cexx8x)!XGOIj!3!cZ*5H@tE6r24N2x+K%sPL z!3lDJ`;&#Ubt=BVKg%8tZx+B$#TP+oMnNKiqsnJ`;N83$@UxgKt5Xp}iB${s?tL8ZqB-hAYi7W`)u$E$t& zx(Sj<%VWeH<2&AAUMh!?4^(6GAG_Aub06Qyuh|wIE;=qudXP;v5~z}SXi(ih0E?rM zBa^A-{jExtAosK0IQ{HQQi2sA*n?%=X;rwhl>Y0r$OXhXW&V(Y^Jgdm)L=%uwfi0T zoH0w8xK&*sFD1(0|FdnbtmGm6sSa}DK?>T~&)@I9`)f80?s|}TZdOt}-))HrX9+Yb zYO440O@6hFkb1ZdoOsIb2qdF+Seqk+|2W0|;-QDyFCO0X{0cRJg8h->pzi|dtQ^{c zZ*rf(OsPnBcfDr%MS%2nDu)Z~dp99~DfJ#|T*{M2qGc;=4_t(7H;D1YfS)0Q<(+o@ z?8dPBeVGSc`&x&xwyBqm?_lIY`;=XW6&xI%6h7n|FiAj{6p$Z^oJmtk&udg~G$G($ z*~RTapZ+?E!n1byrqLUw03ZBW^S7CSLCN@*^hUMQ&cG@ludSDVtv$)PN$TJNBFP@z zn)E8R-RAhLg##g+s9AZ=4$m;2V67?uY~cN1(cLJtG+bZ!ij2qI_I-^P-(`#~puuW| z@fXd{1A1^`xI`E_<^gQ^qo!0`q(9hbm7#i^PH<>v@hHUprZV;^O)1BZ->Dij*+LgP zCQ!F(LtSjpwR0tCXk_LTKN6y<)y7u^P-#=q`CfA<))j(+Cwogc*LHSQWZ_*OV6 zriPyCJmJ|Gp2DK3gRdTduc}UvhUTQVuek--fpGB@H03Eg5;3;8#CY@ zCJ+VudNW!2)6l>FVTmxH3RG$>#k`1H3Bf;9$T}A`vg-?dYJ^RTT+?RWH0*&3AUtUggg^MaLAeVpe@ ztANdzO69z_wE|pE@dq!L!WAhl!1utf$!e=aG`MT}SGL{R0 zy&rLw0VFZz80KC%NfRUix@{D(w2PwDClI3ihzx6tfzgIq&3b|A*g|q83|A`u}+x8eRLb?*dlbM?aiIU-yAtY!pTpM|M5UJ7=yGMj%M=>CwM14+R+ zE&kt-@FXu}^OME=yrUVY)1$+6l!5h(BKGf#_Mm3Qwl^=H9G^A z_O1CXwV-#{&*`1(Su?@a!NmAVsghYoXIDgg*iA({qYw{EQ6JXS+wJ|r#?E; zfVH6_3KGQi$nZthZrz!DrIb>sPijo*L@sZ?Xwj6i3T6w zSVph=?xkCD+HG@u?A!i7yBLNqTDk}K@2WX$G}Yy1S~7d`cbZFJ1sX?jG`N;32&2R* zHhl)*00UfEgr`emeKS%o-Y1EncGaEa7JRie6ysx0xeh{GGo$Zve^gsg*AyQ2pY|}- zIR@*_{*`05sw_SE3I%x-mz`%u>fnjKEI`7z0N3a(5e3hLJuyugWffarCW6l2Tl)IL zXG`k{P|J3n6#2z%lNXZ=#FrM4aWo*8Ps{Q#yF2jT?On_j4Z-*Y{`F0F8xscDxqHTO| zaI>J!Ht~RQ5=}N}bmiwb4X2G7qRykGEh)Dz*@PxPCnG0E!|6wH@}Ie!T4*cfd@!g<-4 z>GMC1E5$Dq+tVy`x08obp+1-nDWKh$H?Zg>lGd}#WJrX;M3SxvT#PWL#rE5L8pZ)f zJf+2h0>ovt->v2f4^nu(qz`^#Q$x;*A^POT_hjQ0M?>Ap8KSbQPH*cMgxAL&npEu- zOdC7(seM&I{e=4W>u7Bg2>)tnbk<1Xr^+|=a%KAkhbUZwl*yZU#4Z6vQ6ejkvm+0X z211J`Df(TewdJFkdWp97W14^a)ysyxVQ{7X5z>{{V<(d_6njTt`qiq+mb2i~*UWQS z4#g7)XNe8yd|qmH@hWynkw?Gkc#000#=y)b4~oAQp=%Xe!EsIya5xnJ@1cF^ae#B# zbc^#s8w)6mHUhrRJdNI0yw|7|8`!HDD~tn`Wpy#O4;M3dC-e{LQ@Xf6gUMftWWU=# zs^j~KS3>U8_mnLI+OLUi3*#UHEXe>hPVfQVi z;uo*M!Adh*){pD#VAZMW&+WX=pV5yO(HN5J#~(Z|g@4f}LO5By)m2~d?4Q`rAtfNA zUoTvM@S>+gqxv=aeSanxWC!ScPm2EF z;NPC2zOtiCd9;i8#n}O$1Y8k5wrxFM;Td{q@TZ*>KBd)&hn4#sH!Q-6AT?WcmV>E%bYf-uAjHNTVUG@71Q>g0LwRt&faHuHgoh2mX(r1`9%)!#0Y*tC3a6C;*BELN|iZYz)>v@5^~)z$%@JF{hpbN|7$A zd*RwQkNp1xqQXOe#a6CXZ4HtUUF7ebZcI4D8eFvd+<63vuup^1 zGlE-4^`KX`TUCbGfToL0&XbGyUVh)M(|GHw-|{J&sfI?)i(T?>U2 zh*i4h3(<+i??jpgCzX@F5%3~)QR9Q%Uk5cbc)+z3td6I@=f?|yBwCGn>;XsYl+)%C zSvAl?`0*Pw0G|9hy8PR(a^c6@azQpHm>~{l%-FF!2WF_4CCmV%sv8og@YVpBnOjwp zJyxVkJoKNx0jNRRuzU)>R+m&Re ziggwR^NiR_Y2`X<5u;mEqW#qnm*RJCM2a{f(&>~PU3Y^t>SHSuI^{Q$kBbtC2R0{R z!E@{d>HwMjTH7qGPeFD&{BD~a5mKnvp~NvZ~&5%b)ygV)|3veS0LKP95eRl0Z!-l*QXiS|413G;bxO9a)7 zH`uM(`Z{da-95Iq_o5e(Y@DoH9K*m-d4f5q;%9 z!H1dx**&_hI%ZMMC=c5>O6^lpg~Pv=uSemcV>JEbH2R$I#0q8BKsrD@4U6aWIQI@K zV$j0}@%--;xtZ8Cj@9GM7a!4N(gQ@{#i2(QyOn91?#5v{SE|kBdz#>pYzI+4dB0E* z3#2|Z%473&n-;ImM)XO@A?CM4o01gZw$kyh=TwHdH{P&ld)nVP2t+*#y;koGj+ZzH z;yi4u1%N|*q_d9or3C+oX$`|RB0i;sTud(w9vi7cb(4MQ z-_ng#J=-;+S1)l?T#sT|A$z-Z(dcbIL%_Y;Pkl=BUvv+mId9*Y*3E7!X%Wpdf&5FI z&Z22Zet2`)sEWx}ae@X0)O7ZZbP>hO79NXT>R(3@xAVrczP1y7gkQl-Ol^siQLP;7 zZqnTny*p6IE}g~Z6@X2~rgS=Q2!?>DBv#5!&KT#Q-3Jnzw$TNP{sF3+#qgN;f4x$|2m1j={C@z_e|#Gx+2P$T zinvxuA&U5s9H|}}+6g|&d(l1gdrT(2q^J%IO;rkiAaqncDzqZsT4cQy##pO(ZxEHu zg1&?^Zv>!Zgq5i=%XPQ0e5w!;QeJBM@ z5j>4jXV>YaYLRLG11hmlq*;D4K4CTrZ5KSVs02F}>|5ucd|7`PssK`c#r3)&M>kzM zD}%{e#^~_KxPS-esncZm?^XA+fl}~*jDV5=?+2I}EE$5?aL%~2$TH=%knMRwh@nFv z;Ac$uwYdQkKmgN4Zip_h1=^uha&@MTU69o$iYcz?J4nbESBxcQztw~uaix4kmmB04 zGQ=Jzic#)b%!F1UhC}SKDkc+VfcHfP$Y=o`mp{dFPO2+ZR^GMFbAf)n9X+?@ev0d< z_f!`+Aex8s>iQ*Z+d`H}9mpr+Iusk#R1b=qFE{J}vE;Id1ShEwHEkRX*@0l6L*M-4phr9ix{=|sV1h)%dt0-AV@%9(aY}r`Xoh z1%K(c4?9LVRrk%!5ldBbY4s&DfVUvR5-Z%|GZ4%#cc|~Cob0+SMDoUGY?xW1{72F?vFI$Evf zkl?+HW~jjxfPtw&Fpnrgwmros{RVm3d;q(usj8Gp6^Ts{zJCAjLOMrfW5`0iM7%!cqvvlxj(^XDfuG|)K|^wVUr<7{TW0Kz zc}-N4rfMi?D=UgyJzM+THK7#*Ea&tkR0{vLkr({7STIH3;cIX($6g$WYQ^8^KK;m& zAG4K>2=0UnEz;gBzx@%>Wac{o%vIO(v68YOWwQG1-V z5>5hcN(46WfeRwZ7N7B~LRI{o_?n3=T6J-#vVHZXXq;`KLZ#ywO~4;a+@ElTFXX8_+`Q$% z>4+$%%cCuDz!p`+w>dzYXUxWfhu)m#5v4YJr?&mT<74?w140QnPS>WQU_$upLs#@G zPA{Vp?bU$!9tcR@kcL7X%1_#$AJ5$3u@HNwV&UPrJ*xjZ;SL8U%~f+Pr3QlF$aj`7p3=_Oe`CwQrwx8Hd?<%)bZSzu3v^#E{Ce{QP7IiK z_CE~I(0GSik7deHGfHK#;^xAP_;sUf1k=T||0@65nYvl|wbQ*dH1<*d4{VW&UC91x5MiRSqnXVxI)c4C*0;6V|yA0R^n{9eJ(A9d?NIRaH zfQ38Je+PrEFUU~T)o+#+@PsJ;dPOD-0wA>=-Tz5{{sS){YzX`AJV-2g)s+^3s!1)_ z-s4vD&4IRB2Ftg#|1drhx^q1te49&v$YK?!g7EF|8w*^e4mTzL zx*PKX9@91)OQgx4*IC#`^~7s&qVkVnVqfh^dF&r(qSExOdeGs79*VG*Mxa2>fSC^w z?d_YAiz>8qsrK8n?N#Ie*FgEpf|C-y8BD>WTJv`OzuTD5_5CYfGZz~=pHj1u5@dpR z^7eVZQ^T6q69@YT1c=dUo)3zs;M}KGMJaO}DWJ+o0;X!}k-+@L6irl#M&eLLEJ(1~ z%0cKWy+`-Qv48)zHue>ZkEY(>Dr82Y#^IrpIq<F?&0*5!df8R(^Lmz^WX+m6c@ znb09?ddH=0T22z}pLl-_C`nTNoe46I0Kdy7G5dHxWejFT`WsX(-@M})6^vTy&t)K{ zW(Y8yXTt`a#|!&4DtR3SWuP+Kr?$~xsz{vW#7A#Ng!J`4KXE0h1p=~=0bnSkyTFoy z>AMNRPAcoA%Vw$4o(^O8fLe;H#TYNUtaulKw1Y5`$(s*Ifp*CRvG}MqY zX&j|kar)z^UaGvcOfeWs#f#tpIZX`xPE|_u^HqJ2$^XP`{Z$09KxNUF1j4VcRWL+% zp531i+#Z_jsq0!O<*F5pR2DgOIH_buv83W`H_tHUsb`Z|xrWjQ|8d2?xZ3uk%Y0l# z%&`;9(E&Zb$vi4Hftezz#9!`7p0swdHCz_-%=%5eS*k+%!VC878NG^V4_$)Y|y_ZMH8OY|Sc`Du+=1?+T zsG)7RGYGm5I(FI2P58-C)c6C<=8vfj&YL;5)U|jVP?a{gUKjS??7N@RVfvdjdyL!T zc3hgGlcLWlbEYst54_?Y1GgNSjc_w$rp*!RW$X_SG<4RGg8ZNCRdAR_p}?kq$cGA0 z50WQCq^T~A=i6Go>0dNM&~X6P3S5bIoK)=#e=(7L&Q}wRlsVL_edQ?oAy_!zAwjV) z@~OVEWJj9=fUGzN$hpG$bJBqjG1PD&uFBi1m*Q*HxhsbCJQ8u4_J2qPjvp|zm&#sX z#lj_>h@aI~+2Sf8BpbS`GM;0terAse)xzcbYa?>MzT@;?7Tjc4A!4?0C!$&--9kT? zqX7RPJP8*SEIskGs3K+&sL*75J$Yn`Ee{$ZUf;uaEm=pMj>&8w%xyGJd#m5K!zBUU(oyNY#v8|v?Gh)W~7S?6(5rE+2D4jSlHq%uFlA5JPY;w%X^gx^wY$3!hdU>pfB3tu zrfi)0OE3}zThi{+fl+Y(OrZGBxG^GD3&t$jpfk9GY!B1P_qWIOdg1j}eE&f{8aH4Iw61qtmF8&Zb*jXAx_)fIYptnBo_DnotmTl;H802x0%RBtbvZogh zC*b;O@s8y$A0qh2j+Kd_8jEl*t1Toa;(}f*RfJw--q{9-QiUumewX|r!k`}Lg}-z2 zH-vU-?9Vr^p5eW~+wA>T-(cvhE=%LTR(?hDmz&MCkn0h7vMUMCr{~KiH5G}HG&`)01f82V&4tV4=tPlvDGO$>2N}sKPCr3jAZei_ZON$;Hwz zzm@c<%hBP{qmIGktq1n}VeSnBwSNP3Qt7#$b7IAxh7%_-7pd5O2oaQ_^Fu(44Vik# zvS;BZTZvT`$&iKm0O-n*vF}rZP(42rSxjuy_{SvpmCD#`{iJBQM`>d@neFFQq1&0;X#4l?gPYUc4chtZ z8ycn6u_B7B%GS+wPJFM4ppsw)m%Xp;MDj4sM5U@ZME1^H4XKy25nXB|M=ycw>slLn zF_8dgR(iBb&QJ4>iqsm9>1alkT4pzV19ktPWt4!Yb4gyFjrqR#=Rzq<=!}LR5QE?(ngS1_%#-u_Q zPc2ZR9ojBhk&;>~rC+hBIBB^^f#T-(%M#q7?=iMuhgeXTIXZ);N+`5~BO-aCGTTH+ zm-|FA-sVc<++WZ$j5Pm8lEh78y<(}DubCdFU3V{| z(DJTGgWGToD8T$5EUnHvYgl5aG7p&-G@fXVtwIK0AM@{+k)o}eLu(!(WKi7hc z;A=0`!Yael&Y5hC&ncfHm_-Loi6+3)>Mf!y3)$%&bKiY=-qHV%{p z&2@B)iJst}k|JBsuE!H|I!z@W4@Lll4Gu`p$I8>XjGNR>uaWWnJpN<^T=LdNn%Ddj z?mvb(OZ=dM4WVkQVBN*e^uff09GkER+n5_=`$A8MSZJ z^4aLNkgd113C$1X1pgl74a4PIJXF{WTI}_GggCgRdVLJ{P;$wRsS&V8klksv&?_;( z8VS2|TZkNY+ganw!&*8Jk`cZ8(~>p}#9s$vi|OsPb%6A?a6ZZ?ZN69?HQiu{$=*;t z)y_Sop%9Mz>|wtL;UoA?v`I2f-5S+(HQJA7RN1a{3e}J+MoNBserv)M1F2@BHz#>S zRpDn2g0ksSa5h)=d;}3Ed^HVzLU(Rp%bW_N{)qKeyDBqwx}p;K{dnfdt^7=Wab-%< zt!W(8%mh611cJqXcQaNCVZp86iVpdMTp$;)-!01%-F_#a`h`|jg|mN>WnV4ruj`e{ zk=$f)QfZL3{(6az@TyA9lriz9DkNFJD2Y0LW3IYY%*UOaJm6$yR_Tw87ZIsMV1{uK*W84$CLxbg0p+5 z4~AtRS5KWLE7VTyZ+QRN zv*&q}$-Vvq2=~@$iSYTGoY|Ar4|^fpb0NnreNL8&u12w^SC&{}78PWmvN?5VG3^!`72Z9|;iAW>IuqGYP z2zedZ!91UTmfU4>gH5v%-!DEz%oZ+Q2IEB)nq!LsZ;iwNxr2w*Q77tzw6-q$`w@>0 z5Z}(?yDhejS|FN1aJbh|dGJ8a^r*OW9MYHekOpi;IIJZLI68sw|CF@4fFJ(hCyR_Y zgtU-{{qjpJz@_?)6a=t+{@<`g1a=Hs58zE`RAb8XbXY^eFGkyX#_M4it&nC83p4W_ zIxsf-FlQ#>!LwW+tM+)EEF^-o3b5F7T3}V|vU~x=MVL3hcbCSYrKO3ay_Kcb@9`r0 zG%m-9oq7Izry-WWiUef?Gu41iB!obFD5(7d(7uD+7vw%*VgFkotw4MzfkOIEoZr!2 zjl%LfJX-xf0vG9xChnH6u)5WPsR7N! zps$y6>mG0FYz3T6JD4RAr;f2)w92Ga@eE=i`kz2$u$6o3+jyU%88$f8PkquiU1?ghJ;s{W|-Pc*-RNk0lwo*Zc$(S!Z+2!l8?W)@Fx%R1@1Z-0c? zy;s&J6`D_6pJhC3dvzO(WpX#B-bU^@zU%wBQHQ* z_X)$~Us_u7&a5eTYYe|8BryaA5@z!}Qb@0omMQ1^lEBM2R?0#NLx2YtrmF|1Lf*jZ z(lDl(Ab7X4{*C_nzH zGwoPs83iyFyLmVlxp;Ol;4!ti|C-ialCJfHWRE zbH)#HIP7&_G!ZmtbZk-wxBXo&j?dA!cgHn|2y@eupFiUBKJ*2_ba4CS6Ao^%?8Yd% z?4wM$W|a&52MQfw&>pYvcmZg4?sfCOa~-$%p+m`qU6V{k-wNq8A;1+%({&|1SFOB| zcdBK(|BA{|4&?GB0cCL7{<`9Mz2dSd-P?844y+)6;}roYo9T7V`Ps9vglm7KI(bD_ zyq#;KlL(qpY~Ot2Cj`_7n?zNIe+1aN@;gh;6b?sB8hP!_EK4FUPU>EM;#Vc0_zcEG zg_VHAcAa;jhkf2_lN{~QINXq>9fJ)_!fq~8fEWqyN&7~Jr^^n9a1hqEncQ0;6NUTu z{)hqaRM@G{4nTZ<)56R%f>OzrrMU^FUy1o?GN~da%TheZ!9u)1)N(}E))}X>Y|*gAdPj?F+QR}t=l5=A+H&JBo5X!fo@HE`SsU>N7C;k$KG#Y`rmi1VRNi*wW+qEPt;NjWiVpb`oDHv2E0e<2ZYf(F{%RWnr+ueB|P zEgp_Ibgnh!95XSLg{=C9mbY_4z#HDz=V6i(tn!3QOTe3372`WMSpJGx}98-IOSE_lpIl zpxVAs^x#ncJ6@ow3sfiuWC$u%v}4R7ir>D_&<<2+Gcwnw9Y0nd2yp0G<1os1W>9T& z=l3ulRoQv_f}P67;x2i3sA3GQM;JAl@h|?qV_QD-?<~MSDS>}@&f}Mme+(E>T?KBO ze}V&GPG|+=mjG|C<=y|v68!(LA!D?l&<>w5O136NuIovGi~Pv>QlS=)H}B$i9yw#> zgSZ143gto}71qs{aKJP}Yq1Nz%xZ2;P`XIzyOgZu#tz*`35B}*^BYOZE53^|UnLS_ zrYDcpo7vaPj~`w^X_%PPSI&4;vD_jQq*!F8bbq%A4bi7X$#)<}4wCk?5H&)GAa3&B z0g0zSyBT>k7ysWwa&mgv6tS%1Q!O-FYyt7DRNMlp9nT~T!Iz)VGera~4+u_EntAb+ z0mQ=W@B7o=3gk5_G!c!$WKQaB;1Z>tRr)PxH(~PT z%X!YrRLFCBVD$ZN+z0$9pkBRN*oi~F@PFDXHMd#VB|syXs<0W3S?A|E;yhGj_!$}U z{tf5+2ZQ<`QDDVZ(zKW`%|YU$VbL!0#*WwWecneDl*P%d1sHhY9X+5);$KqivjT=w zYmHZUWw=T6ESJ=dJ8HNaLa5bKuruERFbE4?Oa0;J@zPYGF}xw_l)KUvBuV#rQllN?s#FloPEA zgNKrIU%i`PWK>&F9ehNk^@DP42L`j@tSdPGH-m%Uu1cD9tfvh7Vhp>DSGU)}8K^Gc zOQI*1zTaM82=kIw$*vEduVjXg>?Wbm<9*%F z+jGBw`MSvu6W#f%YEp}>^CZ$+R{}e^!rE2tDAV(Wj%`kPwMH4Ruu^{uwWRC2UIiLc z-ewL&)EBdiEPXj&M)-{U_6WEh8z=oi50>j(gxsh8CrMsQ#W?x1(XzVmI5&5HW8RcI zR=5N;zj5S{K~O}B#^8+Lbj4+N2nKkz*7BNWw|f2YOcl{KLHjA#dP=6&UrLYWu9&du zNZUr_A&12!r0)eK_am6?`3nmQ*lG5U8MLu09$S>;;I#QD^>E4vi^gyo*^w zp$lLvW@&59p;Z?p$?0H%(B6xXUIC9l6=QkY_GPDYUq*|r;Qu#22604p^=t?kkts{5 zL*q9_+nH%`Z`*}#zpv3nVhbg4q1Rq3lu)*S6Dte-i1o zF;#1+YT%FKHq0)a$I6|t9c0175EN zst0r1z0xP~CN01JsNn*s!=x?B5l3I#i^ysrVI$wZVrX>z#@UzWdoUJx4g{=)#|Y~- zqkj+%ma0Nb;07l)R~?SFuj>5)%tK-z+@MyQMV>zwY=SDa%zRwx&yCO2G8Boo7r*PuCvQU%f%m-YK0=1_gK3RMjZBFS$9aJ9pA!PieM& z^NS4*0lhTxsd=nIBr(Tx!bg>AO=snkU%;Q&NtEsd_MOm3_}JIU2Sa`?O#rnKZN-B` zV33evD+(I5wKxB|<&cqeev+0YoASL|*-o)!2lpK8y{%PN=gHO$#CSo@Q^%uFyfN#g z-+1RVR69_*yZ8)RCDm`MIvh5p7{Dcesxt$U$r**0)VKH8!5=h{u|xu>Repjm-3J>P z5tDygEww7w=zRY_y52ghs(0`DT`W38x&-M`Qo0vNgM_rCfP^$Gx=Xr|?vO6&?rx;J zySq8ly`TNOzkSYm|7O8;0dwBp7@slj4~x6AK~=TCa_;bl2+il4DxKOW4MtgcEEeW> z?zO61*|w1fn6=KH{L(1JwG5!|S{t10oa`5~Pjsj6l5Vfe+Q`2>Z#`a&*tPIGTzU>% z+?{o3IUP<5Gzt+scPXihvZ<(V>yoL?D!rOJd4u9dMI&u6a^+}Pq3Q_EWH#+xdl;Rn zc#$s~sVNtCOX;3K$~eusUTLJP$E!1Nc)LQ2e)!PEwQ=RA|7)n7 zQ`2zH=D#MUXA&e9@1s_O)LA;he^G9L>?81B)Z2eoDnRymMhDf41*~Uv7^RDgWFC7K zi?Lw}?5@h$33eC05K4@lxTav7C%BO1GM~RKs~2(ZaoQ4v$!OnK>|2!TvbBaRiGf8p8TT zO5|tp6Qc#ZU*x?WPrZm|Xr7%6ppL@m|4d9LDIz+d=o#PZYY8L%c|*{D@NARIqJefe zp*YZv&Tv z{B4&Mp@f2jxbN}sXzX!0Z*qABy0rYUMOADeBDI8675AyOb@nu1VW`BS=k99KLz-*p zG@`CsqV+)KJ5?>4(_KYkvoC9f#F3HaAyk=^kf3}8A{ydbq&8 z4J&^iE0ed+frz(i#Wvk4F!d>>`);JvWm*IEhJ=5Y<1L@%?O1Jk%5%~_kts@%yFa)^ z;va43pjwi{6=T`TYsYRm2~ z1sq#z%}$oD4>&D&>y4fwzWRTz~Z~lF@DlggfXxaHRi^HXFx_w^2uOi|L$K%LfO0X zKK4y7D;|)kFVm@CMq`8c>1GDyin6jlL1GtBQfjcgWUdTQPU^734;PW&2+6B&*YXZ0 zdb-j){jT}Pdp7$~Px0hfi9XVGsO0XQ@sXM9pW%4J&twC$h`|n$?8d1y%H8pa7UY75 zB~WKnl{Ob%%s49HH9h#28Iuz!Pp}P6-J&IfL#$07%uC)EIXM5S0GL$~lv7$2$Pj*a z>X!fwTy7bq0z(eq{VS5i>|R=~K(ZP{|dZoI?lj2Fx?$E_#BjSn}SH?@=r zj3K8*Qd0+`4X>$kw^k_@OiEN3UzT z6nnwmc6P$;j+;rb9S=+q`N6FsFnc5@Ue5g_QGpZ1Zo=(4d}hCJ>&{H&`bdd+9AxWW z>ZPuR?(jj9d@IU3Y(e-ESkiWMw7SA zaKJ^~Ga~1&AsJtPyNglNd-i6wwvh54pZ&rUt71W{Zqv{BcC8XWUDBE;@a-HIkXe6g z7`@I(C@*}Z?ROoTP z#dYu?aCz$7Q}gqE?920M#YSNI5r6iXlR(59G=H)`k0D|e4SoQLCdIpgX~*6oDMSl1 zzd-MVeU7oWUHZcG@Ko(J#vxR!t+pk*rK=pUznF4j;EM0-HjMiA6Pl@`A@UKR6%8Pc ze%e&flkrBkMqbw7bWsIpKmv5o7c~a8(nOJh2Pdiiy;FMN-;+RzF4nPPv0RZY{1PDU zORszW$@D8nlck3|myy!fGSen9{q}A@pCA5VL{HWCy`-3wMNjv&fZK@T>u>s-U!Q*B zo%!G|xw4@c*sz0sW8_cBF(ydwu`F+NyP8gu<3E(bNT1ewD|W!lbd0zQ&LIP>su+2N z!n>Y{tx*AvNb$hu7VJg}E$S`4mIoDS{L5n6|Cb&Fj6yHre@7utT`(u;U(=Bus`no$ zq&Z#~v#$RuUC<^(>Yb${U<9K`@@{JNy&7S=TCw%uS}LEw*|^e&oIi`sdxf#_iN)jO zxLypa@UNqzMkL{2y|3Y7%RZ%k+X^qjazD3KAov4%$o5;@w{keB3#=^t*#5oFUqp!=NP+#b$#F%bGTr}!mk8AJ#uK`=Me^nRI$6c<{z_)p z-Za7__OZJgsh(_bP57r~RbBBV)G38EQaSkNvEyvFDEjRVXiSGr6+xsGMp~iD39ilr zW~Iuy^2lTQZeJvJ^Rs@H;|5^143 zX4c=~{iC-_V`x1&52xgvg+;y9doa<8O=Y}3g5&q2V?NfWr@7hRC*>J8S<)}qp{y}z0|;-1i4UQ@B_t-7~Tmi zHnytJC0i+L37pYfpen`&jz57D$%AVx@Ln=V zkZC#S4=_nD{TLroJ`?jDWbdh|*Noajo^KI&&TC-3WiEdVA2$KRzXj1LX~oD&e>2Ds(GZH}qiLsqGu%9NvR0chqO?g;Yy8Lb!hV$(v{$Wxz7w04gmYhi?wP+$zmwQALv({Ai^f*=7Bwni>C% zDk1ht>>d99P)sl|k3DnyROnH4OLRX}KlTty!?{5u;Xz+Ep&{BxB}(LKn2P*_4A(;9blt>5idm3MB^<7K4fg7`u#lVX9?5tRx!h1|Xv*c|vf2w@ zV^Tpx=*6e|vo9VXI|F<`UHXM`gN#8`B&SrvlD;*DZI@j^<0$wv)Bt#kX1bt|iAU5_ zfd$M$dksubGYZH9*efO{-{9uPqT)UX@i}ewad3sYlpFf5lm)N9`pWn6!Sa@c7XS@X zl~jR>qLE#77yp-?=(6?F>+Ag5-Wdv4lVpu+N4dC%V!qd4zaQnHq8W87 zW+?@asM<9Qw954zAGBp(;c)W!{*JHuZqKj*lg68Y#uRoi>3u^Z=QYpqz3XC;{{BVt zf*IQRgP(_?j0^-1NSYv2?~)R*%Y3VDMnc2eX@$QUCNlXgV>%hobM-HoTs56a;WjDkcu2k#Ydsxt_K3p_z&)vWQbIKR%{o-O~G zRnxPz%!5dAEpOcMF9>3zjDpR9?@24uNl^$wHm?+mCF%X=!N0l^^tV9p&4h&|{au}{ zeEdrg+)SqoC;Xog{y&Sge*^sAFj!E{VRk{%QZgcB$K{epXh*kakA#VBfN}}!d8tNu zVDy@m(76V?p4g`9MWU4>dI~SpCKm|(vl2tu#6K6+MNkSp&6ktbdmejS!3WPG+RoH#NA=}`2FW|EVR_v5~=u2F*pIPG`I zX3~z?WANSMO}m#VF+2xAMe6iszh zxL+Hx3oCgKi*n%<;iR2(B`0&L-1v#?2~5my>xXyQY_oLi{*X*`&%7~R_!0-%|FvmU zA)k&X1ndG9*=qo_!XoVSOPlLr-%HbtM=PRkLKgP+)$!+s1**lH=WYbQVWuMOE^7Q~6MVV!StqE;<-T-a;J`?4h4iXvK5>3#A%-iA$v z8%Xp_zZD9Hhr5)VeNVSe#ZJVs^tytPcQ`}yr$(p+J|~psRYymc|QD)eX+b49w-r-u`!=8gWmvc5JOywDrWCWKUzvp&gu!0hvmtj${6kxHfNC)-|M(Ot0_{y(X`DWqeudfxC=HJlZgXqz zaNqI!)r0l}>najLoQZ?7xbI8s2Cv`uU(HDT3FZ3soGV_v@1FYhb#O6Oiy3XRz{QM2 zUb7*?FP~38y9ffig(BQ_9F=h;UsZ&c6YaCxZpl>UE+Enh?8 zuu*OlL==a@A1>bNV7xjRo6epN#`+~!6bAlIyTlyQ2X_9|O>*8*3JRV7F%^?XdeDJr35bb?ObR3MWu9Rw9}g%MU!t zG?A$>z?kB68dH@6EPVY>x?*MR_h*z&my|1RO(s!yf=Qe&SvqWhS#zQcv^o%h#{`G! zZJ~M>A6;$6yc&zN?ZC1F64nq@Ez^u(zM1Q5HQ}bkot>DgtR)bg`F?@cb^k3+vGF<{ z+so*7!_iy+u29%_m}%)2y8+$P%78Tg6IwUwnUH4$c!!Y|$u#xLpGzzHa(C0iUFL;F zTeEof-sh6L_}M3B7I`~MM4VZN#)GuZ)*~QUgk_`(yr9ndYB0rm3AIR7yTWy)#m?gL z1{ebr^d6SGg5+;n8UzH@m_t`|mG~)@O41pm;$aYPPl$ff`%n3S@T$6u7xe-E#OzYM z>QU&+ahA5cL&{c6+6TZJB}u39UaRZ*$p>58r`$ks%a2- zO$p%}*1z(7>%UHXt5UvXj(YrxualI(fvC2!Ho`LGn5GBi}6X zRY-4Drj3R~>3mO+v}1(P`&wZ375Bl;deM4uI(j-oU?r7L(=WJhyg` z%UZuWgh@%AvIn<#j}b!pD}a}Q*!dT+a_9{|Rln6jbDo{=@zS_+zuO5N+lWHN$d{)l4HU zlSdv(pF9_B!c?8#WYA>9$G)Rl$0z9B`Q4iub79)p53baibuiweshUsJA? zvcRT9PKM<;P*v-X`SHRqT=%{STg=F1W*W1(WVy%UVEdV}JR0M0C1 zOLMvFj6eu?dv=DJaq)h5GJvb8)paCs->resKG0ftLLEl_jx2}|v&v;Gmj)r?5;SyV zgl787@$Yb-`)n7d+34Q$d~1EtAyc-`7Yncv*>g|QQv-&DCem-XlA_vo=boGa5hP(J z;|;02_RCGyiYn6iUob>pp|J4{r>0Nwsi?vlAhQ0)akA=(1R!2Zd9aaR@sxA`-=Z7z z*AShE$BTX8pZx-YcGPvx5BptN6*QckR@b+U+_{@Z@x2#1obHIQX7gC9E#u)&V$2ya z6fb&wFSY=#cMO6LIoI=`U|JrsOz|cH1?O!2raAYyWx1yOA9+Lfr#?5?_P6E6H&dlg zQ?->w4%R%Wv^=X{611ER%Dx22&U6a(*R0qkYW$2Xae|f}ih_%ZI*cA9zB}dC^(vK5$HR5>Ac?a;)6vsk`ltWn zuTM7w{(0i$m8te@NY@JL52gh&8%yde$aH6?Z(pMjR*rCW>jtq3l~1=q94SPf_0OqpIFFa9x`f zXV;dJujBOYw|#G^T>Q=99Pn2W>!d393kHdsWto-GYaB2Bu5Nx+;gMLaT=oPwEAdc5 zm-5N(cdO#X<&t=?FY>m_0Tuy}OK4*CGsT*CNPLM;&p$OKf0&b%nvZMGbqEWQ2`Ayx zDZg12UPSB%htz(H^`tkMMB3N=av?b$d?J3#=Y(-_4)g$hr(cvsvPgv7=NLNW)n5#SXUDlGY01f9NH@tvAR@=+ku<2wJMsqE zOK$TnbzJ#jQ2)X9du)-QBi|(olud^+(bgcPAs;v3Y-5r|n4sh1Dx&Q}U3-jq<;Oo)9EPaTBpqWBKr0flV7(Wg1$n#XEV$%I!tW4Mnmfyg*{m z*6z_F5VN4`W0xQlp~2#pcumBfoN*x&dRg0@7^9}Agch%=_Z~5)KlhfRpTj{SQ2X_b z356{sYjtQFXL~o|s)4cG@|XfulhNaW)#Sh=t~wx@T2h#ZxZs4o>{NbR@p}j*{%H)R zXM`C=EBs2@y-i?ecwaZEX#d@m6|Qp7)(6B-5Sc~+GT^dY@Tk&geQr%&l$r_KU3u{i zx>*Scmr{}LPn56YU+%RyRva_}R2$oh@h2{B=;B7_^4+7|l<)u!&=cfcZ+{b;U!Jik z-=CSr8T)CpQPx`e=C+U0Sd-`Bv()@Ad@dHA2FklPWvo!mci2p6D>imBX3XDLiX)Y9 zoF01+-?q8-U`}e&5UT&)@uH6U;)wYX5w{Vn88cYv^6E!c=60^3LsOaUe45B-ieq>_ z;Fxd>?yY(u%;S(lmsA%8-3aslw%y=X&27$hy z57=!Am@Fy@n7;LXIfiK^ne-8)7xlbG%&%r|qAc)iw^EEJ%Ti$6Kmy!dS1Cw%Aszbi zU(yTCUjyoN(&`^XI0f@+c_1H!Zh6QllDqfnplFqg7&Hk+xk&NJ!^i7F#+7lW^Pgi4 z-QXb^JX>oS&!)P7rFz}43UE474iXpMD)?qqQ ztY9GbdHAA?$aoB0shEweNjhQ*u;`atWj55sErg}O@}!_tY&CJ?W9* zTDBp=?GEg{NA*&qi|~XQ zj)0QKkP2%ukSGrP{8mVoisu|!{ju0op?j;@iH?e1SX4|*B{^2mC4!eBcsceievqk& z<@d{2rbe$^N?V8Pn_9;E^UUwY>1|?;5D{|$AjM^}3tu9RMRbBOqqtNs1 zuVJ?!7ZX4PZn|{#bB_54k%Bon%jfwD{6Vg^kNAS`h4_;+TY1QTMwz#M+1BkrC$Y6X z93wxYa8ZYZ(kYm1yiK<~s>*srY9`jVZc#}uSooAYhC=2w+PFdwYG7PR7+_pJ?)lv7 zT$lkyx!w?yI=cR9u0|nM!i^pW0&UwAIOslqwyLZft;UxyMQ?eX+R>%*xn-kKH??ou z?U3Mq0Ih!nLHwtfjGzIGmaCDqVe|j4T>tZ>|Kz{uhCu*F@CmGx?Aw$ zbX_tWh|s+%tdN`6h~PIs2+fNv+^S+8+4prb*T_B(Pg*jZ)Xxe89C27a6bcc<(eWyl z%il{R=!uek^yYsDO=4K;tUAb3!8XBpy0D1#UGK*H=nqkIQP7x zF1#il=-L2!GE?qdC5))FcL{OinAmrhCQ_6^r-)#QFFwXT*zY3CQf>1!14kLbWEMTL zsSPLoj>}?L>8#OM{99h0;o|(7^4@YZNxMghglytMSNmv{mF&@b4-~9ZJ&&0=>J35J z?<#m59zcIRLD}REuQZ~pDy(p%M9S5PMCqE)V+WX9ZrZ&9@??b_bLyNvaMAAMO_4U~ z)(e*mGLiYU>_A<|=co_<(sjvEP?vXfdp)lFi1Goe1&&5gjLg_@N9QJ1iJuH2)K@zf zMVu$I;~6*2H;7y(v)(uQ0?MoobgQ0`V6lK~fv7-=@8QP@>AlWhcskrXKP}WSd-C7i z#S;??_v)yaSQ$9cm6lT+#9_`7h2UniUEfD#cf6^!;0mr|L{7AAs|9&MRgfRTO}r_i z?c%WP7)K;N{|17p>h;9Ha$=G_JCb0`hW6ZekD7Den}&1eALQeQX?H!wBn<-xa)t=B z`JpEzKvaN>biRu_h&mNnabQnQ_D2UkXWXH3%wG#^S9ureCGM&opF&#)CM}XRyqxeY zPd56MnS1At^UU(l(VCw3FBcSk5u`#>VI3-n0k*hfO2=oiddaT_{_#T2e$2>s9G!v5 zuP=WUoY->sc=pJ2?E;fgvBT$Kpo7Op8_O2fEzX zJOVnXy8yQGG(}|+nm|#J2=ucne#KA+^y8T#pm$2P)ja2axm3{PI$j~0ug3i z9p-0x;00N=^lIb((@*_Z)o5Gs{i|*Wa939TU&{(rI+5;m8aw*X+b6xeUnoEe~h zlOo3}6QPvU?J=dAv;kSPifukwl}Evp6F)+>ha|kIQ?L%^62FVQsaJ1+rE3Z$;u2Y< zmnPr#l38VcqQUCSkvu5cSOpRX~Di^D(n}{_@ zac(E&QoO^V!zX&P$xV#P>G?djyst9)CK_SE5|q(!nFvNMQtuZcmy?{hLq{L;-vC1y z5i+=s=OL?h%Zv`y$P8F_Wpu8(&5gSQJj#q@{xz_&p3Jg zb5gAO8&vRZ$BA+H)uw<;>)UbvwbL-bYx|Ydu;q4PYgg;WZMvSPJzqtL4C+O;_D`}N z4CYtsDVai_HbRW-7J4LeY`(BuhME_)3}SpJGh6XELyV!(2{UAduXErn7#eY6*H0wm z`)NXt=#~m2xhU1PnG^kYh^uI;izLQ1sU*Cr z12CS}>}EcB%NqIWL?tT{Y}8QAFwJY35HCnTdae1Al9N(vCoN^v;H#r)MyEX?F_JG; zkab<;%$)i^8yaO#;hVi0MrxVF0ol91n6%#}o!xS~c}#XakN_D+{@rrwM{4{`!g^-5 zn7+C-7JT!Ezn)tII=)ir9{}YSP@Uqv^G^_j(d#%BMbJTErR^e!|6+OqciI6KKy%xr zwNRsS7hsVcQnc%T3#O$5d;JSp66f7Q72J=`Jh#B*il8%hQj}X4C9-$z@!;leYA0-@ zt%5dmyoXY<*o=$l{lksGjtG=ItcWw7l&QvuZAgIVoHXRCAjny%6&)#4AjPt5>Y{Tb z4aBpmTinP|mck3)K`XZhV{7~KJ8kFUl%LnvXQ*l(PkNRw2kJk1y%825q{Vghh7M0h z3Q2mka)flW*UC_n{2_PL+l#61X&8w1>Aw`{-xr;4yaivqr^1r~oT2t)GrFYJ94t!N zdTl$D5K$0BP3#c1Rk0-4I-h00yJwG0O|KRz(`XCy&or{QlX7KeQK z-)YzjzC$@mHg@$vg9r4xZ4b`{O9ZDxe(>zd#B(d0qt)E}^x>p6K4G+kC(0wMp_9gj z;uEXR;G>L=+=%g-B;3jZdUvXvfu*yybM4G+Sn`LLEL_x&Hnag?y?8Pjq%6R7k6beL z20lZ;sXfG%>Kj7}=TY!p`jEzNy*b;F+460F{61h0wsxTFu=h^Kn^P2L{41A50!osA zC~`Z66mCUb;-3xoo=IUqR@ta%Z%eB2vz74zNqA+s+Pr6QGXB_QlIU5WR!P(JvRFBO z&%xZ-vALWGEYJRB)y0D0MnrNBcXU-|)mT>#v)DyjJ`(e0MhNz9{lFog^f%YBuOm-4 zdAxs8u;julSnU0U`%PO3?2F6(apS2ghcMtPru_Cyy^W%IVF$_zSJB31$}4EE=B4y5 z+5Ql;V&5ZTtyxNBZKjJFpa|^ad?R(z(-c$EKfLIBnsG&CbvlzgpOWL37BBLO91Uk zsVMGpOX96!p<0PU?zUH0X}OJr$^Te;`qhDrpEQ*zVz0N26=BG`*^&5s|8Y9{&nFT; zDoo%7StUHyl1RDSmW|rlLOxh`G1|F1n2dw}W-)O$e%_)3)pN@1?87Ati9PEJ8O5#y z-{8NyZkC}}y!iA*Sf-lHh*Pd^yP7?WEgzS9>cWBW=ns}uL&63qL_XgiF9uih8+aj9 z?_1cv!QB6^QgT{L4y?yK-%K8g{+D+04~xm|4#nS;I~3+|4t6}h>+IMnb`)~}Lg52c zlpn3uG$DN63ADNg+!+bJ6&$JDES8-VVvgORb zD;ml48H@j0>t(pOwE&t{6-HLLF^A8#kumrB@Q=g7D3{31z?{%=_HDf6h)p@2Qz+M~ zSz9-b5uulu>hz)dku7KzYQF_j#Eu2OoFjSHz)4}-#D?&A$`WLK$~TvZh_#}O+;Vqv z?@!_=|5PfdkJzS4PWb#c6&DN$i(_2HzIEARh@Abe1vz+xKlXDPhRW=L<6EBT;3_lP z1!AWF;s65=Dk2VESrNg}*87Lw)E3Tpl^z9NR)BeIx9~fk0)b2xj|bTLtwP(?JxS_U z`oAViE?pc)Fl69v;-m%j$GJ1~wB$7aRPabtVj!m@2E)FnCW834ak}6FB;LSwIcHdZ zE=wwVyI!h$GibH!@jyh&i)}p?W%X{qYw%0194%=<)@fH;b6gyxh!hLJ^D_q(-JaeB@uXs+W3fvJ6eOtH!of>NjJ2rn!K10xJ0j3EYR^b5Ab z9#(l#%@6WHKx9V8iIsEyO6pL}C+8s*rl@e-b8)EDD2PUX|d`Y8lpoU|b4&R=p zK^)^7LzO}R+H9`7tJB@{LkljS0~Vv@RH7DT{^!`zFFCiumoBSkMgjZ9NNMB3UG>7S zbBB1;hs*~o+Apw<{Lj1o5kuNuBa5uYE8@=1D{i*~i!spk_td`dQWO3I;y7<|cIqA~ zZ9Ja+h1|=Z#lZ?DopXb{*P?P_votj299mtGfzJAut9d$_PJ6?ZMqh10XJ<>oY0u=0 zpme&p&t#jn>lb^VFd$2*X!fvdNXvO-I{2)ox(HIsOXQk~MPdmjE|wpSA&lCHT!kYu+7eor;eJG=;Y zzYwvyZ@BXttA463>_OTj>gNeJnettStHIfhXV5TWGSPos=c*w`-^`iskSag^VO`0JV3eh9KX@YQ4ufNV zoSq8NMMivmQ@crm6&|s3hyO8iY{lk&`%F3R&c!x%S1uA~7uRz9;WNSp(DDPhC`nh6 zxmh@N>`iZxFr!za*hwp(BOm2c-Nii_jyGy>hQ|X47*IoLV)vb?GB(A9qsOLLzvX*) zE~uoNqiAuO+O}r~JC&SO<3GRprS182?1S(s<7p> z2tW5d7Fv%(OTSxjJw{ZTZ}ufK+5IvxcNT5~zkgkXG#>H=GxrL;T6l9Y5DTt>dQUan zIwmjlM5nBMg^b~T(yWg04JW!6{^{~hkHq{KuFq~5%4HPd51yb0#sI3J)_j;1KmQ^% zAvPD3UvDyLx7K_tr}WS zHjqnW2$)Y5R@3!tsE1k^oU9QDfEnC}svDn9#?hCkHK4|dFYG(n(PVn4&FBLPx|H6Pr56PRC#BhmQJuBsS9#n1VY7#fIJB)Es^iXJ(PUdR- z*mPjf+3nm+HlPkS{3Vk4r?17tV+hn`hXBu%S$qj0p|;EMtb(@2oG^*yyCE_j0r%dm zs@IE9$YY$xkI40O>9Czd^3v`bPvIJ%(0xUGdiL=1y^>(UCX5qNNX3diFVT#!+2Ssz zLlj&%%PPYX-AfTg{6M1hBrG;*hl6% z0ra}jD%DF>$=|Q8=3W?X1e^ailU_BZ6mptwbLIKYG-J}ix^UQ-PKvnnE7}zT;XmRp zHlNTk33-(`AyFg>!CjNnIQhB?MGuj01=f+0f7rc6Im!yhb@7aUO_(L<(tG5iiJ>2k zFWfBz+s_k)J>jjv-AvYxvZQ0*=x0K_kUy@LyxltS+@;Eh9n@cpp=Y_@^W|*k5N?+2 zZr>lN^;LJ9N0Ydu^c^keY1z8y_7_oI7Qf=-5=~85GKt+#mM(CIgsjZNM^vMcTbC{k zSVi?~`V%ad=#kXd3y0WH$1Y-8tYqHo)p)GmJ_;_cQK)&cuAt5+wQt;WQ><848$&TB z-oG0tfysy3cy*=J!0o!}TpGP|2*yfY4@^K0FVec7%^s|Iu8-GJazi4_>A9W@KRelm z0V=Y6Yg3E-7Fi6)QX;|HsOV+)*bS&QtEdC<3ha8{Se5pgIrx2&K1HK{jzZ`^U|Flu zXFQ|}6(|ai+yO%0rjDK~A+S`fXK4^nPi^MnS_B1JInlG&*oJZ(351GHHd% zHEQ^;wRxkDl!QYS0wUvB+u7#h*W`Sp|NTMElptKL3T5-~(Sf?McgF_t)d!80?A z!L47U!cyf;^O~>6e6=1pEi5rb=kALe+c2t<1Iq3GJgRxb%O6aV_NuFpFb#}p5Q`B4 zPMh06WOLu(7i_TC=4XL1_%24NK-bSAMv`Z*@*r7R4%#Lq@4R1w=z_nuu5!@P{0^ns zG?}FcO`0b)!${cBicwGy`qptfKny&()WN%Tv5HM@FYZO9={%KP()+qBcH(z|l?dBP zUNcKsZc$5e?`tjiyzTaVF18`S$e(Dpfw|+-@CBoJmo8ON6h%^bgAH*-Sl1h22jqR@ z)z+UFKI)>ca+bZia0s!`A#1*f#dPJwGDm-umMa}J4>?LW?NJw|* zo!&Sd(_xnua`G<=`tn%nkZA|r zy&Mg3#MdZU=9`=o_8K=g>Y0?%DzEB1>Z7Z+R)x5@);=;VhNNeC9`I@lU?B;|gq4jf z&(WR#&|m6_o!GPN2TGeP#n)$eQ28jTN^5#Hu{kB^6H6;a;0L^s3UR>BV&Evt%kR;3 z3Ou_A!qGOQSg02|xL*A9fmp!-N(k#}XJ)x4Cu~M(O@{w!@A zf4ThB&~kdCr!f}<=@eY*#|3qlsxuV6S8$S#+!U6&FHf}!e(Nczn6&NnW96O}3&pMg zRwv^4j|I*ha7?$~$q@=2cS}X^IJ)X zA#)cR%F!^D#(0h|v3eJ4*L}x*v_dk^3vE0{*NL@>h-Lu%r{Z62a%R2lQPtPvO)r>d z^KBolRZ~=bT~~K6c*PpLph~+1T5|&rdG~57QnWhT#fa9Y`juEA zpB$#)YX6%3O}X1bUr?AMAzYjN(x_+ z*;UAFtW0dnynB4cLq{(^)kYl>NDvz+#8MA`uvvPg1IrxsO{+H%$vomJ#by8cH**V% z0piDp^op-{_pisEc;ENBfZE*n8lHM^qdsEVnZ7))sG6>FY*AR0_-3p(#)6#T-zbUv z!jxw9w8MgL7-MvazzM0I(sZNbz%W7dTX&SFe1u2MKg%lKx&818NGa@8_!%rlCb}^j z>#5r(s#o-`B@C4F8h)0Un1Aw}FGY5uobP`x0SAM^?*799{KHV$_Tmbc(02Xugrx`i zzik9TdRM0t{t#$Z&|927f4_+|$Z7+DAl$RgZxce4lYLR^-su?0*-TL+WGG*m>Ww7X z^AO6u(}(oZnnn3KYsXB@akV)pfijpBJ3}DUAlow3p;1~g!+fUU3ZHbu^=^^|9)7pj zIqFBa9Ylc`g~(dKR%~3h^tU||+jtPDNd!iYO-t-X#gvSeJco*S z*|F1`9dcsF^9#@Ha3cCz zv$NNPdfw~0P){*1Rybjh54Votb1YJHP6*AXDC-vB^11@IqxQu5X6lOQ4HIS{?{N+j zlX#9UaDp%~6d5#@ijiu*kAWX>eW3Osuk+49++lF_pCGC^vO^B5} zUV$4lCRrnqxrFFLcT*WkO|CJ_jR1erKqm0xX?xgGrEgBs%Zbt~`Yo8&$-i`&`O`2~ z!(o-l{N)cT)v^Y`puRG6!t%_8<_BvQ?z*^Di{pjk>L6}wB%WkSrm2eCSjWxFa&Dt< zqNkzPhBjvcpjan(Dd|mg1GAx@-=0Ib3-JlZ(jS_3yLZ$@)(gO2;dxL`sshhzxgJZI4$p>4OU&)+X{0zLIkH8M$q5|YHya( z%23}t>>gAK7DGRpwb7ERBZ3!=5Z*QgoUXwjQGxY-zW2 z#Q5yynGHF8AlTqz!1?u>>+>x2^>Ea$PQSabxKGLNp1JUY6FgzO#C+%?ehaY3e5@Yg zVrdAVgh-rbIU+BW3oD?WbLt5vnVc1AY4q@@cP>?b48Y}Ud7Svt_Fhh}2%n##9QQ^= zBb>|hId|IJ<~;trD*wta71SY~YFIKEIK^Q116l&R4mZ&QJjT`j0!KH#x}sVC%Y9tz zTSBoF1%~)!De~rX3r#Me7Cqscf(Ri+W28<*bT4H5Py*}epK5dOhC~Mki2O|m#LDtO z{@)--uOXudD2-0WayCgPCslVe7a-qYtKk%V3G4N)~NWVKc9;$fqPnx zwHrO2`hgI2(Bsd|Ti(0)9%twF8DoJ>!Onwfyxw4YOyKFj1i9&`+xhjud}Or(B)a42 z5T;?j%_pEl^~Rwyx|+_N=if3ArT|kIs&#B4I;GEMPW zOyzNRBCJ;r&PhUU#r(Q2h{gctSI3Sb{-gWFHA&eGsjC<*r6-;M)z7vXIl0p=)e>~K zhm8nzw?mm`r>w!C1Mq_uWuUFrSs9#xbdqNx-;u}d!d%q6rh4osP<)*x|E?)f^5O_m zs#~(l4<=nPb@4`dS|A@@!9PR+UeFs(P`j2{Re5?UnA#y=c>aG`(f>4G|2sJ82?KoH zT=f4B{_sB^$p~+Y!?@=Pi%x?h+m;mum6BFDX^=Ri2;bAOr3iQw6pZt~LPhoDAu1>8 zV8f}@^Y;vuMz1T@I;6{`%7Kdmjl;VW*ug1^!|2mQMXvIcyRxQoxfAiw2zkB}Tc@Hf z@&-xJ5st`UsalTGtp~ARx`RH$f_4WK<=E-gzukpUkQ9?+!bN9>sH;O;z92a0rzQpK!R<^x;73z+id-5(3W!Ole9=VeMP@E{+UW z8~E`IKITEbBPf9d_UA`CQ&A)>Ipjo1I(I_PY&h{of%7Ifck0x#T03K2{na*`+Tp~D zXwwppcC{&-sP*-8v>yMRx=4(3hTN*myE+8R+UuU;fO?qjZW(wf@jQP~8+}F|NADmU zd<)-q9609GIAo;D^|^_a^FbmHapSN^nW#awoJ@hck>}fq3pLpOmS~U4r`YFysNc%Q)6=EDpQ)uNrJ8>9+pDkF0N(M z>z;gI8-kqDhsA{2U>5&gxuoSajth+chn7c$2nCO2SvOimFQNSGo)}Xb1pcVP+m&K_ zBZ<5Ql<@54PNI5v|H10fc^7y20&fU1M)2q%UvqA6wzQU_7yx_mz%2yly|Iqc7QE_eA zx`lgicXtTxuE7cJF2P*_1b3Gp!QI{60)*fi+}+&+Z|9ubeNXor_h%JrFo4?3x#njx zj&t$(7r#NG3FYfGTL8^*mDeqj+GQm;2`URJZ!4PAn+<&M=B8Q%nRQMsIRW%uhzrYm zfsYFVXAbClT+kTUp$2IzV^b|8Z|Kbj(O8Q0%N7-S(#pKblS*=56=^y%Q#z10W2r2J zN4sxilF1huy*($S4APnpgDExJDVU7Gbuem6MaM%S!{q$U+13E50cc^vcK!7yGIm!; zu)PvuhXi-Y`SL7=A;p-~1(SwWNf~^wa{GlxeSh&-*X@k@lTRd8r54-PH<+SN^DhWz zu7j~Sp6e%`*Jn-6y;}q0H|XzL0CjL}qgP-y$M|8R@&Tn;*Dwml(KQDq*%FeGHu$>$ z<5KkK3`xSFPcsj}9QV$ukrt(PgKr{}}*WK@|Ep*)5v>r!SG4pQCwl z-3G7EkV)QEFK@G8=K&L3IIr?AEL^$0gO$w!^~xsi>+RTD0o~>8SfPgdP~sXK6?Lfb z(L4bk17uy|6dL34?|8u8ACZCPI8Zp&`HBWo2A3j+?}XFuSsppHmzHh;>MICO9Tdt@ zOdUNPmEFS0OH5~ky`BK@lAzz*+7Ivpg>dklT0@BdJ%_n~mUHbng-^}R!7G!-to*g7 z{WB@ph~}*DhBnE~hu;FW+*Qkm6Z{XBwJkjs*}cKdYwx7U-_S~@3VnYW8Z~g=YmauX z@b6vK#~x03xl&p8{y@slX(I*sM52^@CnE%1_;5IU^!cSei#d;@;wR)$qR}*717OE( z%7_^4gy@;qxy{ zZ=;*ge~w%xKF;a;ZD1c%&}0_IkK3tM6&dQRPSeM2a|Rb>RV@%4+QHhVNMS?l5qODD zhae;gzOg8iGMe1L6{2$(x99iG2z0NwkcJ7b_87@mdF&a$>exHW{B;WO<+F>MOcksw z2iGot9d{ts%4^?t z%Rb*-Y{+v7ulMy)x1?^Qzrz$iU*;S-9mNb9$$U4&wu^}=?H3VQ&Wr;!L#=?=4y6%J z^7+@|iHbcI{6mqFC{dU${%0sKR)ku4l9~p$^q*!emlI4=+o@)13a=FQY50U4n>a4d|WP7Qq zNo>;a)_sHcU?cr8n!Fh|K!xHvN4miI*D)0$uN9fpCb1uKZn(!N{O4^?bb2-^gNM}d zf$$Yxn>+`M;fdv$9LxZl(I%Y??$gisx$+$u99)*m^_~`KZ!qNOjB*RCZ~zEHPr)eifW}R|bAE<{;%cj^};v-NE#J;?h()?I!^>Y6*98~`La@`}M zYf{Tysn^~Ju--6{BB~4CRF{p%(Cg?2Wf)TCQAECHxIWknX>5E{sp9Ucq}9sIPCsc$ z^W`&ZuX%Wm@2foTlT7eA1i?TU`jtn8ZF>2L{)Ny%@38pS+!Lc%$o&};_#3}nl-~xXq0=HcPC)(9%b~EU{O+>EFzRa1C!IF?#d1Nb2$zFR^)w=L-Tkho| zQ`blaQ=-^8t1Nqmy~~{`2Aj)0i2|crO<}r|v^o@aiP4AgLn~-L4qrzezf_9VYs#!nUdDcxJ{x2(G}_!rxWd~%N$lW z$HPJYLv`S-L0S5P&>9Vfx)g_1@pVpK1K1>BWY=U64R|Atm90~h`DF&g=SSYJKi|R) zaipAL$<}5aTr}B^Wh%UL=F&VkkPu0_Et9oMJ73g%l?Z&b(^`wOjQh&hL{Lps#nf&) zPCitGwonH4_K`$MyIpF1j89JjHwrrw1YlNCzaMtXwz{j-Q6jyf1pvIZ>jP{a}utBhjVeh!iC&2N3_Wo_SOHgEHJV=|Xlw_q*d=2FTZ zw1*tXIS6Of^OmekVb4|Qf=X)23L>CNx*6Tj;W2xK+Skc_m~a8D_{YQ%PN8i-D@ zf_^KZbkbpQ-5P;REOter< zy?(x6q=v32ij)hDfHsltRguhEGM1!@JcZ)06_@;D_^y4)SiNzVfKkj-=YF=&YjUWC z!Q-KZ!2s_zw_S13Cl6!`h8nf6J?J2eY{JhdqOzW`zv%d*Uv22$@{*y5AY~l8V=l2# zq~ce9H;q~Jst9l3<=guG%0Z4d8&v_qB2TAJQcVP(ZYP8;AFX2ceHUwb0pAkxHKkA} zM;zV{r_3^gB8cfd1ERJ^s+QNhNKV*<3chbtVFiz992?nSkPhcWTv!|^+{mzcxdv?g zj;piPl`pbNi}NX<;y1lQDuR&wcwd(9jhZGrFgY&rr|Y;ivHg%Od2*Q!l@Fmmy|0mKnO<+4<)f+0br{suUOdhR;b*~f z_ltfc4um8Cb2i-<2D`F1z44I9w#?2Lk#vA0v$ z3S8wUDvreC5{0`HR9A$7M{2Z%Z{o@{4Ya8IYmHJ6^xEq4GzG!UATWwBLrdXdC4k(F zX4!@%a@6fN!Ca$sq0Z#fFk3+t77eg)OkwiV8q8q`(zuey8s4hsTo^E`3EBFdZTxztrb^PwG}Y!gjWRMYAF;U-MX@#m z_M=T|e_|s0=0|>Ta`+OdDOI~298{BgRC^yCdjnA{6VpLoN zuyA(8+K?2z zg)pepM73KjgG#6+lVxAa#Lh;i*^#LA&b9ntW(?BEq~$Q{9&ndEb&yMtTs$2xQQuM)4)ARzxV~g{Qku?|)EpYSTe_|| z62Cqy+YhC^GAel3xG}!|uI|?7+Z^>jFw~JU1OsX2fWeK^NGf8{I7n{3M?)Gr>vmmQ z_nZs5NV?q4vuk}WV~@KwdqloosN9()B2s+Z;;Fw%$G|u^b>^-@&t3XVvH_Bzf=#&5 zji_BD^VqsHx46yz@{BaN;)!i@L&-WZrI&z;-cba%&L&IjS+HDHeQ)>WpfF`6lx{22 z`chX>K8>Ku#qv}3OP{EVXe}mCnm^?1Xj81HAc>EQ@cBuqA1d4mOI_t*X9wd-yhmvX zLW#&T%1P3Nr+5(nvlHMQry?0yI@7s*Ue5DK7td(Uzhvp&Kkk}pZ2K%uTUDCc&ZFpb zrK;^r1oqT+nEFc=i&>0XPXJNbqreZgP{ngNa)Jcw9|W(dzi{TuNW7pvomK~;#8+1` z)jj4yjAT5alif_F>SzJIrKwV23!U(f3{A2AOTklrI6R!^R$I^{oY3WaHNUYs7rOy5 zU^sfh#8Q7d%>WxfKTC@l=x2S`2|_-9!U@Yd)}#+(g+CxEcsMTr-EY2ec3omoAFa@A z)KlWXyW$FX9yO0}*-R1&%jfd_=|l_|L7Q9@+?t9ty zr9hWuyuJmOapBU%2yLZ2RLl3_6>-yjr7U?cAUDUhl^MsPfV)RVVA!9adkYYKexzSp z<9I#uGWL3xZ@~8(kT2-+n!aCgU*#~P=43cXquNhZHN;1J05*@?&ak^u&*su%sVu#W z+#k>1+AbAL#S78^JINE6g4nZ6K3p?58e4i-DV8uMCr=)#Aw5ue->$k(=sGN9Al(uZWy0Dv5eJOb9IYvA4PpC}qfp;qF@X~(tQ>k97V5 zW#!9%&>V_u*;MAm#*@m*Q|;!#=Yna6;`yPEyZ4cRV&+()0k&2CLK<1;Nj}|I+5Lf6 zIMoKsFBSdDUFfajxgmG|?W3N`3|}udV9|=|)6+ab8qzIZSRP9;xO+YEtTt4JT82_M zERy%k)dd?0Uq>@gmNA!%gkQj{F49)Xt?MzB`9zT|Qu2pMJtn!D$@FKibxR;eZ<@ ze|)u4P?Ga-|K?}^4H5niU29Jc2qzvRl_pU$p`chJl$K@~YM{Wmk{GnH$Tw35NI?$K zfGni6V@<^U)TvbMND7jKsv7BVkSu1%iGaI7Hqsp(Yh80Wesqq6djN5+goxcbRYaW&p?(1oAF1SF!GVAgWQDUKc`JOwQzJZ$=yXapg z7>s=5C9no}kFXX`t`ms?2$BZJkUJl)Donp?|P#iy&4 zMx)U^L&D=5HeUL<77j$Q(Oiw0CIfnp2&xd5_O{DW8OPm|V0Z2FNM-#(fFk}TWadoB!A8V{Id?X^Pd-!l!zPPJ!ju`cS^=fUR&Q@z9lHGZ(P!Ut`_;DN z4(qBJnnJyg0$w1{`Pb|yPFwtzRSTZXaa0&&LRLQdp|;fbRun5hr0&8U<^C4ioH_uO zOCU(^Rc~-FrHAl*MT=0NLxLq$@rX5b^^{q*e~)sY=+_l0x~uKJ;sv+wjcJK9Q2vO; z;=R;0iuAMDPGIoswy?3K^-Q<#1fQPX7}@{O^) zi>EayT=_5#^M5%(dVFaCGNb!gWehWx$xSZHZO8yv?$8A~QbFkVaeH;wQ3Dg<(wo*K4OcwPl6n_y z%D%g5h-eFw%{bTO*|q&0bd{%}_AB+WqmCYzuEM@z3L>tPjll<(uNvwdFZZ9=^uhd^ zKz`RU+9=4Ntt)2p&Ezl1kJK6s3)ScNLV(9l)RDMFJl+*K~J5`>U@=H=kECz**1%b z3x9|^My>WU_R&JqtDbjs{;}5Almaw8=LD4KKyaHhD7*8}Dfp8jx*p6_NKnFN2Sw-C zyBZyM>7jA zFI^(11r7K;p2;Dpu89_Pkyh?XM`NrT#iP3Y68aFo2ZxVuyKxi52KR3|Ho-XF;8S5P zBr|Gl`8f3kj4?CmMSxfMda{pCF#9_F+8 zdOeb1i)K>Z{w5l{itQhtP773)o8}bPc_@o(Mh@*;B$#<47T%f2ONg8ED#!2PQfx6l zlzq#?ID@Qp7AJ)EpEg<3tv4UqO)Spy5*F>vH#5{4YyJb~JrFi)V7nd8Q=@NHI@-DL zOB%qfqC@TelTu_bGr0i=0-_Fz$Zj** zp<#<5ar{2V?2HPQYu@{17O#$<+ksZ4Bz_p?? zuzH&AzR$OzDGJ|CC>5054gevG*iRB-R4->qER5`W<-$3vUvAz{JJ3|Dq( zTRI_O-Fx|%>T}FEXGiMpr-Kpy-YObkufPT*mXK0n1^1)$vrfmYs1y2iNNW@Br32we zY3}4P0R#Naj3R!hRoR7S#6+6q$#y0a^5OLEEIWnb@-Jlk7-Al9s6n=6L;{!4RR}S& z*Z8D9d|{D%;qkJf8x`#0NQ9VTP<5*W?r<5d#9u6$pTa5$yOG5Sr6%jxcss zr^jRP<@nZZdu&}A_CHpyzeNL+zvHV8#=)cOp5*3X0_8{*%NyVAv$I$R({#T*d33=1 zIwRK6K-Lj@u5b^WJrN1aYvP2L4-!F&@L7vH?p?`gV&H zhrBEWRoTQ18xJSb8yX0dKeR1?z0^1T)xI_XY z`8-7@Gps2}bLNR};>gxaqu4YfmB9HUS`_S|+ocO>^=vgXuy+LP#{RXz=p@`BcB<&) zdg7N`_ru2ym^Vrw4PXmj!9O)KjTl25_t$&LPT(UP<08y;vt5VD@iFjq$0TICRiGyx zQ}1mEX>HXYtg2{G`97uoTIltw>YA(LW=leINT)1le6p>F3%Y6@%IjU zasShSX2pO^1EVK4th8MNvS+0gFR6QfxBJ@<*USL8`BfW-^SG)kzC{aVmpta+lZNjR zbn?9_2h{kt5n#u_+^PY&mjjUZ`s&(y2?3Mm<>|8e%Oc4@-=`PqTu+Jh;5@Lp{oOmY z97ivP8WhlLCB+%Bd^h!IcL%Ep@2?i+Udv{w!L39?*!j!0OnBk>ORu|RieYO?Kqo$y zPA?XK5SbWWd#&;rHe-VWbFLrUuT;DVchhCqbN|&wX_LJvzsKmk!;SMCaY3`fe1W=xu6T~8DqZj{1Hq=r&{YOf4rj0Vbb3EVu*VR~@O zIg{^cf)>Z;PEFUKc5WBQs2YL~ZBdo!(i?XfBRyu}vj z$&9gBO;3+%1I>;@DeccIH3!RSe`6hVpte)$azM(N2f>&9vuSoY4k7TpPImFI?-XL z;5^aKTF-|)?YpCc#GDt%Otpv9TYbFBp=48DonJcdu}o*y_vr!OPt#&OO#ZUOH;<-y zE!^z%g9VJ}@8HXq8f)-)od(q8ADuEuJ}4F*^Y@-h$8`KeQKNFPZkpQ9wIk%M(0uN@ z#@^xhQMsEsb&U?^Ge&kGOSQ^rbLn*)FK+MFQX_xCenzQRVu_cnC#nrBN87xq0}tPp z3QRKp=JmHo;eY`@vIOB+Z#FiM(tliApI#nseG+*^3a z7^B16-JcrX9*mzV6vHC^^&AxxGZD^Pb@S}yNMIv8WGFLSC`!{Hj_GvK%B+%;4U2b} zKKu+Z;XgsDP%h$+$V*bWfA8&+ymVH;xL{FeBq=b^bC#grnKYNqncMQPj#>K>sqDQgzcS$@etdd`aRv@zNKUE}o}2<~@0A9- z_gqul!u}{{5ca6zxc5^{c~M0fH0h+Bb_7d&UACQ__JYdFpXa*EABdJ3Y+B9M6@`pY zxiG|vk=nets)=}O!0yE{3?nzto&cdg3$oknHM6=SuJO;vX@|s;t~KywrL=QOnPHra z2itmLt&4~9YJFocQd>codrmL)N?orRvHB@_-zJy~1*YK3Jm{)grulrat@!w}?bUp7{44EYs_aqZ$f5W$d_KW<;k8l*_V|agd z@vy^3bk6M&rvIItOjGES0O$|`#OEoc;6B}G&C0b9^qA7o(INX~y#;KbZ9f%Sqwtf1 zc@@KU`*(ud@$Q`T>#=+(4e4U{19hj}MlV({fFU?Yj)B6R>UD=jiD=DX0-5HM$XJ+8 zr!+EtxsR)QC7JFS+6W$7u^Y0aD=P%6I=TD*yYT{rgsJ17|~9$n|x_`=F8oZtCiNzFTC3gP|hJT7`=q}7f^LNs0JpJE~?W3zMQi#@)J7q7onvc0X2-a`C1 zpgde5n?m|m_6%uecO%-*o;E8R&(kHWarHt~qjlJA@s@lydg|HiyNrg^$R!1&xcdkh zZ7w@V2ZU0o#8s;6LXuPhE$=j)h?&VMc{ZNzpwfCSXDR6J5 zfAtjs!%7TOF@Yoa&_0yK~%1i2MZLx4+f{t zO&$A`DbL7isyB)(ZyX`lBiTiqz+Z)27TV{%?-M%(YrfZ|jYyLHFldHXe&T2<-#M~q z=*z==n`<;<6>e#p#MJf6V)I(V*|%;zK|8fAX|zt(vp-Y9m3~P3ncv2Ki5-iQH7N z5!<^N-TK}w!gMmVR@7pqn8gY-)fKo;(9(2J;bF8=eA1AHV*mp`(8Y2c=K+DFFT^yc z>e(F1Hpxh_gRf~CgfD$()i}>bxAF+j?H4~M>~3r}-aFR8BND=2U8xiDyU@5HlJ0a? z|Ek#2A@GtjnBjh_2{(}Ow^h3AmLUt8v814%_nYwL7lOZ>sQ0uZ2_hikIACZwZ?nd4 zvRibD%n>1O572ZCt%jKOLxvp|(?Cy=Y7=S@xZqU!{nGAPTps3fc$KnRdl1{qsB`SeDjPviVxBHLa7*OO>9`)-xr6S(c#L>?*>7KPF|Nj5DSdZvALfw}lT&hs z+@5Nd_nKolq4$_{nAYA#>p4Q{=tlcw+x5OT_CF@yoHTAVy03Tke6*oA^Zm=U84$2y zFY5hO*FhV3CEtW#?j=&o6vs1z;(LQWxQ@yuyQ-KVrANmB&NoLv>Z6d({q}RE0$|_X zjWQh~uzL6m5ObvRpM~A2KvYRp(JVRPD7ajGLZ^PWaz7J$-tl_Y&XcxhV0G;iyL~&) zEEe$oE}ENjwEei&^ZD-AQ*7r|1K=Ks_o3lcb7yaRZo+Hn`{PO5GgeIX)%tzHCX})H zHG0MQ&UhdQ=VW1u1fy#39%GN>(mV{_q;d?%wHj;11HXW0{+VhI&G0Z7Z0&7dY2dq+ z87t)Ius$KAgK$Y1NR{l%Qv>`+JMfzR%=b!WlyVs?==xs!ddH3c)-#{+ZMJlV*?&Ut z7!mYYaEe0njR!GfMHs_Z#&Ha7&p^h&jgGA?*GK&31KVn9AC-6+0|RqQFs%q3iwd;Ci7q@clN%x%bg+! zhtBd+y?#{5_pHMV)Z8zj>mmi2riqY=u@2w|NOjTRTdag7>VPs4ZjVlark6d3pOt(Z zk)Xh_Rn@zv=U(sQ+YLC&atpB25qC4lkHLG7{OC|0Df6;rp|y_Q_3@ z;qPt|7JIb$Gds>+YkG^<4|`+rEvagl9%>@)!anl9wc9I{8Vb-eX(=t*C_nk|Mr{*7 z(+WT;TwED^sUO;Thg0d1Z4Ikg|IL16JLWkMh1X)Fy!y5CQz8{3Y!APFl2kFi0af=z z=A&@-3c;7LXPsuacDHkX#qK9GxdKs)*;jN>Tu-Q}6vl5#7g1IOQb$H*(aFrBub)P5 z$vh;hCgPt>{va|y5^Kpr6^XD?hx&kH!##5M%dl^g`~4Hwe8;}G{WZ1FE3iSQl3{_; z3AL2Grt^11EzEi6-8XR1RTGJf7+df95>}?L7=d1B6&BDcYg~t7zAc*zT`ldedk3DO z)A&WhH^o;(ZueVX6M-J+e!rT)!bF_$iXQd{>{M>&ScTMev%H5->*<=xk&E?uHq#)R`P5Wn=jUinpG5kAFdW>TP+sklMA^&YF;I`SE` zMvl~!&K$`%Pjh3;l!U#sc`VltIkCP`y#B>t^(@1AeU9B$m-8_N$VPZR>v}4i$4*MJ zl*ilsMXQkX=U%h=If{PBe|@78%#cON0cRkL^(nczegkmWEYLVzpQ2YY5>$NJ9J^lE zk8ieCq|(X!k@m;5r-4WeEjE78o^!gmuWz;gGQ}w};M=X-aLP;)L)N>!Xe&SzIr+QE z_idLr#a75+>^CyQx`F-|R*+$C6@R|%(u-`vM(PK{G{l*TYi27F2O^XwH;Q7}`AZ^4 z6jDfpdNUwYpZ0b6)AvQSSg}y#_0@(6+*IH1eR3`L{XTU(HoFF+;LGe=eGMs-3(Q;B zIhcC}oKN6FQCY0J(GiA9aw)#Ood#v-ns?EXDs^WIfn&2Qp zSW^3sGOnmBA%Ss?MlzROk8Bth^0`4dfz}N_2=|&)BnTN41YaL~?Y;?}H&8)I-SiJf z98n%{18Fq#dfweG+SFCm3|vQy5lSnK2!;wFAj09uHOf!+6e{I2l z|B<-&7h2^1X7k67DD?bPGxe9~+@A)JIE~DM|6iR52i#D~aJj>}`CdWbp5LWam_-^{ zKS59cZ&K~)cPIC|H5gFJ)O!0%R&}VUERouX_XId)Tm8rs8n!w#_y9pJ@zIXDXg0ag z6-B})#G!;Ko_T#$r``8qPDR|iHLiM z5akk}DV$kNz+UCCmC6W&&s7AY^m)ZJiD}FJ4lQX&xh{_<3j5&z%Q(yLFC-V$T`y-} z#o>e`XVYg6CmSMQh_81%2uYMaA$O|vN`U01WhnrB)vt%DmposoMjutx8vzc*<>ypC zlhLf`wElOm0)&c|REbI9VaPwCGw$R65pK#8ZV1p?$V#&ze|Mnn-Iw579iBs}H|JYu zX&pj8G|~H}tBH=4355O@53ONB+M)1YnOKp$2Y3T4S9Cl|pHso(({b{GhY%H#$ox~< z9;{x<8+i$jMsB>rpx<-@zLBIW6-7Nr4v}PC5XXdr4@y69pO?j(2Jln|xIlh6am44! zdM}lcElBQ$nQ?F#Cs+~`S;a`d^(#x)z#%6&%mo0EP<=pghBuhhrW&YZ-tRa)dPez# zh$DjD5-Js)UO^(<=VEIPC)i=6`Xf!Sh!hlTVud3=>% zfUh~*fbV>v?bBx~DQ?>Z2lymJR0=q#JBEMH7Ck83GQf39>*-x?e1PGl2oZx;hs$)Y zubo|As4h)W@9(qwj+wbRyV^KaG4|HYO{y~#M^WUia(4~R8PH&>pH}zXW#uVp2)Q4d$wy=;U|&@qL>o4l@VtUBtvDA?l#{ zQa9X)fVWz-ctqsLSSvVaL8H*$X1KpdAi5C8Z%r^7mfI+MzV<>Ph59KgcJ~L1fpE#G zDAXQ$niFNMk1uEIvZ>#9A4Qz#zXWG|jErGL(1XuJZ(ZP;8*(!(w?UIB7u`G?9ZbsK zEnMb}p3>p&P$`lyEUxbv1boYaSu}z~%{vZ^WE$93jY->ZEHJ|%n^vp456_`|p7_($ zLQhEk6YlYU^Z2U(zoONGvHrE>fe?(U#tgy#|5BJD`tyS8z?))#X{*YTwDZ6#F0;Y( zz)1{Xz!D}^;(Uj1Z0r98X4L}|gmZ>)Beu3nlxn-{)i^4IO2d!GgRC`lzNiD}1GsqA z*@jUwMcK!B1SKa&g-taq&H$&jQnjQJgOUdVtZshnw*;dO3dY8I2DO3MArm2%1XwiT zIWZVcR1#^#ms}YL#iAfmes}51^$%cYk!bdMy%+AJDd-=Vi-`c-h(E*w7tTtUW>kXA z-1N+)1YnoI`qecvaNVw~)KKB0COuQp+LW+a%|zrrLUiH#$A$TQn=bc5ahb6chfCYL zlU6xJ96xz44I8y3aFwT&_V!NfCf#+wE3!TK%nu(;mnKfM?hdlDS3?|ev-Gh|E6JOXGBs^Zh?D@V|AS9+GHVr_;^egEUDET9sDwoc# zEsYj*y*i;>JWw)TbaKCym3Xj`50R+BRY2uHxZG=khx&;!=Uh+{t9BM>REwn81xn=euzv*9EfH=d%@BkW3x6o&)xkJ0YI!>B1J zG9k-8^UImyg?TRf!pk5~hvTRqn=%`zn@-bCsICG-hF_oQ??tVFYsw@g7s6i0*XWc2 zu9Fy2wRSp*c#cWVw80ctQbe(iiuTgBWFNV<2+^9K?zd`iw(CFo5c}MqQH5-f3FEE zUUhz(*7fWZntk9GC~4&_mFLH*@L>oAglqn|2~+ZdeQoon}R83qm%GC+udQ4c&^K}xx2K( z=28@of;pYXpd+3aCx#j2Du7w>y>Jc@*v#97V4>W4q<$lVkNi*HRTr z%225*&Slp&_xe<@O*1`v+kvB67D)MdcF^EIO7hi5+M~B-CvgmZ>m{ZKvfnIp3VL`0 zfDtVce8u|d-oV<)s?9u*`VN&2L9Qri@OzAui9z+>hGfGxj19#y|5W;sb+Z`rE%Ghr zrwb?pHI<;g1-a)~<4>4L0Ox$&+H7&SgCOY`5_@9|l3Md-abJ-e#8rJ^g9Wwa4zvYaDZM#-X>PoItwN5W&Svyl~&UKxZMA=p>>>G`mKMDqjtQUsW@sce_8`WJNd>@WT z>unv#CqWbrCAW{YR2m&|8e{@8p16`ho-9s!EnNq?CD1R&{=!0j`GJ*!>fPOpyN38+ zmf*q~5OG~?$54e_K)PDITnz^*%>CS~<-i}T6z-3p@teMU7I zbBP-=d?Rg-{j4(iE}$(Zoe@-N6sm(ZMvl0L{6Lg+$fG*Yj0(5yF#*xT1-T*WVh(Su zG%K9!JKoSOe!6`YD;;=lZPg&Ei+%W{;nS5^8fZfa8Y26I7+feqdBFm<1OeEc0@uWo z>1S@yTZw-&&t^TtXHb7e28-(jUDWTD&{mw^!a;+lpos2E4f%t+0y@eFNrQ)V&FI^uHj>uBQ4f+}_I`US|x3)#}WSQkNng_FaC&?u1_QhCtAMm;xi zxx8GEC1{5jA#Y`5DSpv)ndGqYeRH_;09nn)iywhVjA9m?XRO?>nlZwiC?~24yAJgHuyT^XA`M6bazrxcP zhE+Q+RSrA6!vJJ`QdtJXQ(K$oWjd=-Dx}qi$kaa)O37O%oyu*$auH&(kvVP}wae`+ zFobnN5R$1#@_kW1%TxO&<}&QhEC^+{TddJQq{U^iT#!>Q>)US2Iiw$5bVAob2(E@O z{RhW9QxneUOAYuNPH@w#wd6@>sMveGc4zrt<8)T8O57t3cMX*Aa|2(OAx8WIcwkcF zH?+F5%*={GNZVg>SfD(5fYgN|d~9?7%+@%iHXd!?*j++$Ag}QUPD3s?p|6iwP%?wZZ_#I8g~y#_r=R5Z z(@Q#eq%2=zJlrOzvLlCvcInAl_o6Z-jw_1)m}c3JDztzdEY0QV*pc7_Uu!ywzszwL zCoNYl$ELa2_0>hpUi%~6L$p+F|Kt>vHiKxwDz6y^m5Rh9+X<^Zqtgj)AyAK;kj@+jDst^ zoqaBhhW@@&{`=1P;`OJX`XZm5RQy#jNmPK_C#TAHO|O3+`7ag83pUxGiC=~ZF+=bZ zYg^ttpUY+PNca{ng|X2IySOz>{b2_kN*ueW&}^{m!8fJ+VYeWc(s9gErL~Lv67&l7 z{M(I;n3Oz)qz>|83r=wTK=y*jT43x&+DCqw!2S~!VX*`Y1+GVvOOy8r^k=%PJDUc& z&poc`N(EfFRoa_wP7T^KkeYIp=k6$d*(vI8SolnAWyNmqQ#;Uw{OatCF1RCVY}Br8 zykrhq(#K{;drI24)K9^JR0;f(LkM;@WCusN>iOYmMN~Iq6U)p+vzD+8CniKlG#(fJ z4wOR;3IpBvh}`7ie_Dd2TYi!(KmW!1nV&rDY7V+q1zT7-C#7BDocHJR569na^q2a{7~8wJ_h8SIEj8>shV=p zEPN8s+~W&W3V6pnrO6z|+6F2HeE&jv#~D7Dg7h5YT1jCIIG8FbIbTF7&rF7Hxek}$ zAbCHEUV3H|TbrOs6cIXngQkL^^tS3`44a;Oy1`ZTPKS#-J4nUcDvy9yIIdk^G5DJ2 zGAAsyuj^xN4}$@8)5 zx&47Du}a8)*hqUl##Jo~uOtsA;pmAjdN9&4#CK;I6@Rug_g=Md>l)}6-HHGwA1k+> zFKO_AD6q=#st(~VB3z>s`06xPA{}_R`UrRomDSYdzG%ciT=Iv5cbsAEMzRiwsCipJ zpy*-vo$o5SJ{($R-)a?yHrraCxj0Gl+XmkkUq*u`^>8;!Ns9RX^l;vhmJ+PwY*z~f z+(XWG9Zn}^y%5jj0nk39riVELvY~~?vA@Uvmy;LX10Y|mHY}RiMU}lq8x{jRAE+~2 zSS$}I?F>UhJQrp&?z;YXexLblLonWS z%l$qwBJjf|<<*x#CuPt}f81HV^j+{m=jJK1@_orTN~<~=25;e4QgzdyI4nC#GlhjS z8X?i~gG7kcDubV)^;Y~dUn6orglI6m}Q?r*2P#29q@kEa#*SMW_jGI z>$nTJW{jAsr2Y8X^wxF!0*?}Nu6k1WdAFIA#CLrTGR;im)GrXC5(j! z?UZzp!7R0~_*;GOMZBPsycUbjzs5rKG>(~UrBPzn|Bks=#T3IC7G=Wn+3!X|B-sq_ zkc4~NZ9K+Ut@HRTt46_b-J)02iWkiloY7O)7*p3v(_0MPF|iKuk&4c;@-&@m0v>PM zX8UX9I*1cb(N-rwHsz_5ccsv2|30YXDn~Gm>(UP*OU|l$+vm-J5t4qLf8v{yt(=w4 zD79dqsaO5C)3)m74>r)~7Uq2zx}_7%F%sf3(_K25(gI(8v<&!KjdbvJ;X2Q!b71yz z(wCKGiVBnFpeVu8Kw^Ny`&+<(M;Jbz;85=t{{23r4hIbI1+ZYhf1dl-OFuhEo#;L^ z(x$pX^2x;!_3Cugflf|Jxd1PSe9om0#xlGNqL|fDpf{}eUkwhb*+MVv1*gSj)TMRe zoVmG~l(VA5rO5NL^#<)5#GlTU%WX1goQuM%(LiIZyk+Op2^;|r7nx%K zC+}wpX&~D5T>$QwKYPir!xH^?310UES&A5UzWx$^gZnBXEa|q-@rOi;nf1v^XkaH~ zm~;aua#pbHm4->Pt|Fy97f5GJY#OLog=q=R|CP^7Rs5L1>XeOa+_r>kqG}p(?Nd6G zLnCoH$~;cK_q%-kV`}*I&aEzMGJfa6Bq84H*uqZvog&)6qAi6fn?@`za^FBW*1#Z(0e$YdnXGibq99?&si!B?2V=yCu*ocnWpm8euVWb z1SL_PZ&28-SrLARp2)BvIcOzRe?DsSV(eKfIZHS9Hf-a5r^CB%y(5RAb*S%nRk>t4 zQ%?mio#b-~)n;BIh_>Cw7R{tY{jHE@vdc@8rySPk^;W*$$3F<*C8mT!NxUp}%e`Xz zXUkFTQs66n49u@o)45{|yN05|3Bn^BL8!WT3QrWml#5KH&!?Me&1R=Ec;7!VJ1fw;Mj((0I4@@80q6llV>qCZDf5eKT0hTrUEV zb?7{3d2t!+ll9g;@Ns;GZ@UbKZfePMkR4`ER9YpMu&niblk}Rnt*HCpHXaJ+cjv&U zDzB%kN0G~hV0!SQ`st4jAnr6ZYc$MSIZ1!2bF*8+L!ylfVFo^u%aYDA+ism8(3Le%-A z=_mW=6cmzcPHuJ=j0d_U@Dt8!-YuOrqu{Mwm?Z+^zujq18}&$@n7vFSCVxWnHBgWL zpc*m>!@-p}5^JxLaay?}7$9GCy5O<+Zr+G~Ls5?r{}Vy-1FA0R;&s6y-7Y2u9mjB} zI70u-1H<@Nw{h`mmzT^PmsM)}%r6P=_=O6Ljok{t|8lYsdD@^ucTbWa{?OFiF($pD zwYM&Hp^<`m`JD}xJ}m&r+VR`Z_{iV7gPwF(kIZGAWL;9uGrO(;xTuHsW@>6CDi&i= z8{_O>b}o`tgEjj3iyzuL(k(Es`jB0U8wQ1ha*f6Z3>{>XA1E5p%qsXBH_zZZ+OSy6 z@cTEtfhVqQXL;f<`DmR|)(RwQEV2dp4J~sRW^u>VZ(~7g%rUHHdlFekHVK|80()NB zvdSy1&YcY;qZ$fUnLmsxoj_8#N^d2Mxo{0YNzpl_zZLNBC3Ox8^dtNAdK6V~7!KCG zb5Qi3ORHGW;eW{Gzhv-_dZLp~0;^yG648c|;TwY7#^cN8?I*SLB$NY<`C@|>yLxO9 zB7n9ZHQ>24{Ir>uIa#&l8M-E0&e#O;F|EB4PGNqsLY!Xi{g9&1)af>R7ETM|3q8zJtG69Tc_cQuz2K zr{gI`2X+9K+bT^&Lyng5RJoGugdHcMW)Rp38ws*I4{*D{6S)ihBHhR)|F{ z??DunJ2G5ee2-B&fX?0r7jy3_+agNu z(bt9~dyRg0C)geN#GDPaa1~Cxt+rPpaFLG{NQqULY*~{Zc63O}Ai1=z!RjU$2)6`$zSh~x@FStWEhH-& zy*t<0^YrVIIYXHjrEvhfvBg~d$b84tspY@E5Mn}{)yG^pQ~6Pr-nrIUL$2nXUnjK3 zm_o*L9=Y&6UT@tXMTU1t*Hfhv5(&#iT4I>}`h8hpdgNyV`?+|j!$k%iH4r@V>ppd* z*K>#Gr#7>>0>@exqq2odkK%}bJdfF;x*op`t^vel`NmGY&z0SN@9$H2kB=kP#EGBPA&vuoF_ixur-k#+4O3dQi=cYIPGW{(_elFtw&FriMVZpZ!*;}@v_2~W%i%Pw`yM6)b8Dgmu2DlpFa-G~u{%}C9;B=6*<5wh~3N12K! z_^)2_p?!4bh|=`;7M?R|XuwBOfv39~Is=7J&>7ELb70rQO|gRHrL8y(!5s)TKw@l% zwiSVi>x{x5;Y|zrDgUaVHrs7J?H)H?wpHM>hD~JkA~c}2(HhlztV*Uv+0pRI@MM!s zO3-ZgQc8f!e5=iUf`W5eY05RzJGZ2?#mOnPP)}%PRVuSuuC3@}<=aOd9o@XX-wYs~ zR*XqXm|ABUO=9X2GPBIG8s?hV_#aJsx>PDZKU31hJ&~1wW$lgpy@A_M1F^!=-uPo? z6bs8`CVR|8PkQjTC|3SDedM*IfsX5rSahtQ?_ZGtSu{Bp`PI}5SmEz&O=>FdU#2wS z`t!qyjkO$=kuRQ%Zama81^A!UP2V`Jap88yAL_Yh{11~EKdn7f*!@gCp@ms0-!J|8 zA0u2gAskJ>Nv%<4Qp(@=(X8hP{$~dKr>OnU-5_^e+F?9uZ{mlEsaf2M^ltUmKZF3Th!`Yq-q``uguy2B>|{GD`=+q00-*2u#d z@KY5~sy6Mnb;98paEGrj&fbzsXj*$$qGYyNb@Cq~(+}tPdgMSuQJ7yu6N^3m%Q-LO$KN`ToJ2Q|!4^ zsqV*3;g@irAN0A99EoIUCjtNz;YHMHJcvAOzHn>%UHbNTqYA2x7{VYV8+LzZ8V(=icLIuGmS zG_?S)`JpUP4KGRBMeu+$U;YNbEdNTiSr$L4ioQwAiA}Y;w%n4#|zbi z8tZ@)J2;d`B$dsTSY!??G(tIN9CA;cn$S)ML>AAOK%x8gYfHed^1WyYpD`h=Vi&tX zf@tT@(F%^Sxyr91`9bpMX2NbHkGG`vwSuXQGi`gEZ4YCT=hw-WUY};`9urV}o!2jf zUP``B9LnZ(U6^){4yu88n8NK!WB#F>2dGUR{g?P?`arX?_GE%ji+cE@C!H-W7F z;d*b77Y88b;+r(-$$z=$g} zGV{g#K<$CC#@16vr0~vuvQ4V8*%4y}h5LN1Tdl&Yg}V_hh;>7fU;65tXFH1#zs`8j zu%XvJGCb*MJnG$WGKCLk!__Hs$;N+VDPuvO&dR*E9?X6a?X43^o$XMxH~zS+ureO< z9N~nCs{OmKC{3jzF45Q0TqaOu; zk~J(Y=a2(A@qh+#zPkTC?i>^8z>*5#GEMxuxp&)_HK+vvzw|4MV%3y=s1VghOHjk5 zOL$ltOx*z7nGx@j?9%|OB`Jfn6DRgky0fmIeVI}nip-W3s716n?b|o^PJ6KDzVzw* zay9Qld!TzYCFSP%-P~8G8luE-uq~%4!C~zKKB`%TiN#9mJd2IO*ICzi3@8QMJo@*w z&em#M(;8G6_nxU`D0j7>Shy}YSIC~#DcqZQ8v9bghip$QfJ8Vs#5#S51>0{ymo#p> z{-gIZmaowyGI46H9K25N+J;#w0%!1$weN!9`;SbI7@X>D+W1x(efnR>P%qYWv!N@h zbC(_`N6+RphQ<3UaC^vB8qQAVIEP)GGU)pOsl++8p-{ph}AyMTWgWPu5@Q-!*AAeyC-GX5~ z1)Ylmym=)1^;r2NC>#L=-VcNwb9fc15zP46B3>Taf*I(i4Z?q@#}4TWEX*T@NJREADymRc~H80Df(ectv3Bj|Wf8*p#uCrx@=5awL1 z%h1jdU2$;un0;jeNQD!WY?`K7l#@Ku@M?Va0l%-)sD|Txnl=)th1_apFktVnaO`kS zAo7_XUHr~a*51$<3Mn_#1d9)sy~iu%C;4A(|JR}iDg z9@#0~^TLLp3P?_a9zDvkd8^hVFy=Vl|1GSkNs~-Q`GcW9<(qvGTxxQHn6>{q4at&g zvj^RHLj@>X!*3)p#%9Yllp0_YcUCG`HcR1UYYQ4Q`(UF zm;E1<;;7AgTP$7$quc3YcudU|C+bzA>bgbLy>3I-JFF@np7mBPr3|%#?EAs)#T5Km zY$cYK{W}}l!-G8fLgb5q%6NKwx4iQ8>2RN`BaudA(mA#N$xvypLm0&P>wqKglX15O z1^dR!venKlTXjxZ%PGF*@K3mTyku`LY~toy2_AdJXV}< zp(fhH{M=$6alA?FT&P&lJRKF>>WQy6?6hiFexMwv(aIQ8kMp=qbW;0XeJC6Y_dx#^ z)8KmKZj^*k(;pp-PJIR+de8q|f6-3bP@Q+JNp=D$#_?lDxu;b8ujmN}-2gu!KrV!c zpSLpsyAu^UYri)F-KkXgKEJ}aST13(w9sadBlPuXfQE-Rs6%Lrn|Z{dOME@0?$Gqt zf?Z1Z(C1?t+>*Of}wpHFI7WL{R?HB94%Y?meOuk&8>89Ph@b_6s+-~rUB6*#;B&=v^l zH(y$+{dGcedZBUU{}KlgY)A{j$#hE`jf#)ldVR>gVg%gB?snGZSjc$41?rYDhfVZ( zPZ*GKgC2Zm#ndO$NGfo~RTl){;ujOf(WV|?%C4(*ZTLX4&~u!`WmtRfDeis8_DHl6 z_#VnBI+)RznAFws(O2!l->|1lsl@`Mnn8Z&OcWjg0N<%P+0fcWFQE=KfxQ{BY7Hkz zQpSbI0v8J#H9|95OM5XbzWlr>Lt2@x)cX7E-f(T6?^d;g!gehe0av!&yw4I1C2$QI zXlf__ksk8%p(;WznGJH&GGqOfDw6#w<@tB~_F>odNU`kezc`3y{wI&{68&z-^a~tm9$9^vgBp2qtY0 z*pr!-tSE!e`5u~Y;x}r^0lLbye|KQ1IG>e4)}ZMZ==*rvMZ41j{Q!iHFM{s@AS|O> zD`D=4H!bisD51_iZfZKR$w&ty$onymu+Z-|u}|5({zK)cnscJ+NsTtl@ghnO>4W3-cDCMfK!;;~lsJXKQJx3u-}|h_N%; zYLccBGx^@9YM+qFuonj5>v|tm`=YZ4rGt$#18ek#-z0lJJCAO<$`(#HoRWJ_&?Ju^ zvr~+LE>PdpuUpOUZYeL9zxzV}_VRLcT>_vo~<1#MA>Gk^D>fa+h zC6`1G9ysNb@<}RCN<04jXwN=^E7z;e1o9;&QHJlgOX)V8c0P6wg1FCM0zeXL$bdJf zRD7VP0?FE%YZm=h`%~YUbGtz@^3mW#=@C(Y^a7#~7oN|_I>l3??Hwjv89u}Y7NBZ` zv%NOH*4CJ1r^Do#-HK#uLCq5FYTDiG6Lo1SRh1u&z6JT9TlIp990aiZnSTIy|e>c(TZmdFf1%VdancV3S~qIhKXzhS$%#m~Eic7+O^Q9!5v~ z!}@WJg3Z($J3t;^Pwe}^1X-EC6s5djKtuC(awkIAS;lr=(mIB}!rWK_Nd{Gp$&!HG zcgbjc?kTNhIt#K63fK5VV};pIz5}#nMNpeCq#K$@qEts$i>aFMjWc)SY|_y8Rkn*I zZ$7Gjmm+K=X9arj{Rt6h%Q zV*1CQ8?V%)L--0rlvjrR>H7L9bmCMkL?*LBq1hV*K}{m8FzC#ewR`C(d)&8!pL@ol8tX08sLC_Q z8>}HF!xZ1McPT#-kW3B2UmVv#rozLO=!`Q4XMQl2BDmr`U;@shbK;j{k zg~NIfd_fAV6=_lpG^fH&sG`A+-|sK|E~!6tSx#uw*(^v0KOnHL`<1v0S3#iS1NuaM zv6VfOmc?>rJ$aHtM+W+OKH$OR$wRwj8O-991w8xhd-9^&uk+$yKqgKR?@&O>0iuT{ z!S!7_rDB!MxU9(K7!UH>MAb3&@2?}(&Y4*Orsr4mMat?4SbByMGrCRkuI-2Jk<~yI zYpj6+#X1ev;xlk?%Hd}(juG-Y)V0KAv#5i3L_qOOM&;-FnLil=%K7tc0o8!Dkr3eV zn?bTdQXuS1`3OLO>L%ZPdhKmoNYD!{&YZ1UUwex<8dN8vue;pr>Tt6s66#3O8hH6V(9L*)U z8x!^HMAsQ)sFR;9MM|Jn1vb_6OnhPaxNfH=eq9KyX)jqx49BmX1Oe1pvm|7U#Z`-B zmk*;*yt~#6*p1gcte0PGmx2kFleI(Eb=wb9?5BRH)qE>CMROY;gkP_9{D@!ex-HJ( zng2<0hb8S1m82;&t|P(^N8;5?#U^vaE=w(6mgvN3vSHLndkHD%V5v@|!h&s4ypuX?l&=tBCR(NAX{f8Kci z<)IWUGw3u__5}FCgV`KzH$BJiT;mScs7`MhAN5x(w5fX_)+O*2esDb~ZyuB4nZhrt1UoP*o6?@}Rn#5?5Nm}i*5n76TFi=y zmt^UZ%(^28QtIC_nLk-2<>w?l$b+Ix%xrtjw_U5l88XrV)hxY z9#qgBdKbp8+yo`Mm*01pYk;l|H29BP+!0s1tgKBSG8H)ZEOn3wp>*%&E83d`}x-*Ic3R72I{jB{{YXUe)JNk!^DiIGke2{Npa{mgzl;5^JY_3o)_$fB+>bz?oKP>rd?+Nj) zyLM`lgW|OY)Icjc=~$z&6n338c$Vj4z@_T(5 znmhVPDFCm12M{$x*M@+@gLV_|uCX0DbfE4oS9x_5#R0!4(*Yd=5O#Z+<>EcD z;l%Q)JAP~@K3&n233u!Qn3PnGfwuuZpKNN$g_MPoG7#3BNV9U_fjm!#2i1?c=^vz^ z*qxFsUMm)U)rVQ}$13vW>!)*lB7+Te!(F^}W7hH#C{FV28IAy~+l8GAsxj`Y#h-%# z8YZA2IWX%a^~7l|;!Vj{__OXGdesc=TWaYHTs1z4aF%JcJ^x z;5{9&8+NYk=9SCe=Vcw+kpou19khbWJl+!eoD>!tPGU-(IAw<2zn~a)D%my)hC5NOiUx=vQdo5CkT5w-$5LaW9W7_qsZ!daO z|0H3^r4Ydakd_*B((OsbNu56fUPPm#%TbtxWthm&ktWpZU_>`=N4mjY`gkqWg(Db- z_TiC_6Qp;J_uUWP_Wvfz*}C#%Ws03UGGL9K{+Oty1cd^TJ+UY1p(sQ*gi^KgQYhn? z;#}U8ojH^nF2Xka4vYrG4UEt8oq75Aund}gdXhe=oQs^tB@3~z9(IBHi?Gpn{^?q& zv{24(%KT$FXNPl3#Au%Ni*~iZk$@`S|33-zM?fE?!20?$sHpK@qvgLZ!Gi2h0j&jj zzYl(x`v_W?NpQL#D~8qhl26L4FjOckcGMEVD)ecLC(_(fw#S}*1DO`YrJ5t~YWVV3A3-Y>+Wg&XP{ zOT|s?%hr|}*?+5hw@5}sA+ufI$tg-hsRTH2Swp_O^yX9X;t_jc0^B3@)~(Kzd1p*u z$e>J^=MoMHBD(ZSyR*pddWrz{)^|1JiYfgJ#p|folGBG1x5sq*VP z)DA=d5fmgBoNKQO zyPVvf*ZKrQ7yYu>&H2wqicr^?(~U$;sLIg0-XP5jC?MKq9yFYE=*2f(@<|TsRWdwp zl+*-}uNQ|DKp(t+)JB8*HK;BO@WjbrrDJ_nvbJ)|FEHekyR3U*_fqWZ`iQ5p9E(2X zzRA-HPSdgB+i@58d$AQKN2RK25Q3=+vUe;vy!xEKE51Q&sn7be*AZxjy38IYO0!HAkB2xF0C(_ z+v{tDqW^s|z!`pC7hv+jUbaM--~LN(Z6tN-LD)O^;K*h7hK;NT8RhPGCLiZ8lE113M%$`z$w9j6o+EoF|pRE`mo(9P);Z3dFp_ zCc=MXKZjNc9zOl;s9~$bW7SbX=4%eO5J>msBr<*9Le7`Rq}*3!7oNzDYY2g8s!hPr zTB?zwYpqN)wg&;hC4Zk;Q))C4_<7vqF*CljNa@oCwKxcBv|BcOY@{>*bC#H~?kDes z6<65!%$%Htd^kc}SNUWX9b#T|^gh+4`J;T?A!rXChd}irZi7WDp5d%)G9{5GJSZ;m zu7LZ-l2tw6%eN!=OD0f?r%dOUczb_a4Ohzg-;$bGhX>t?8wd)DIFvWnR*3e_b?LLZ zW&#(lwh1m7JAX>`=7twHyo}A(ZS1c7GYE%=z-)Ymn?&c?H85O))Fh(}$gY!gA^&CD z-~ByNtzslfHoc>)<^ScU72%x!$l=`2f2-gBAt(VYXx9GrL=avSA=F6SYH=0Z=G!~V z9XVa!iv{R^M=#jI27M7X7W+x~(Kx+2!DbijTHuZk;7oXBJ%HTl~3Zvlhpr#Gk?m|dV?F|)Z-!OtWGCwBk=|Wf3aimdxNP_r%k3hgAr;- zx*YUBf_)~ZTn2C|Tvp0{K^2hCI>k#Su@f!P^!EDc?ju!Lo!n#=;hpTSoeN(yRIkx0 zYpV1@rjHJhKxfj)H!^ds8dB&#upwHBQ=v?#GAvX(Z~_NeCLr)Q@T}zwib9RbRQY!! zPbvc*XOGyEl48OcX}~nDt6GE&_K!PUwFCV2bCSUi=nx=I;j7;XkFQZ78$Y}pQn8{u zl25)x=iyl#!zNnU;gl{carE@Nk8b*P{*<7={1paZ#*`#N6vItZ@TTq1AZQ~*^JeAJ zroq-wZLE-X`5&{q&777M9TqaqC|p%+LY`-7!9^T&*gKK2_5SWK@FDmm&i}8LVs#{< zSGq~Dv0TV`m5U0fQ1D6*d*zgCkiBo`|E`Uwy5?9l#A2f9vIxvR*hx*Uf2allF&)84 zy&)rp^@p~tYt5|z5}_`i4%5)m-!=oEwNT_`>9Q=d!9jZW%hc5>0i7EG%a=pWH;=LU z(xYcgv{lq-u5P%++s{7DZCpE5KjE>a6B-L#N^YWLfs{meuodis>~D=JdY3NlFH(Mg z2nml-J3pzz8+-2P7FonbstXF_^ELzk@x~dZPc{&rhKlmd^#~Co0>57WfKl%u!OQ*V z*I_-MPQBp+8XkcDxR1Qa$NHv)+|!`vh}!R~cXti<2S2)hEOrXv^>mEb)+|)5?bP`@ z!i|g+orYa0csaSY>W8K8tmA90u;;O1_pI&aSvdnC+OFspAoprJ6S|(9?^m3nfx)0)z+t9XK&>I^tPWOe$r6!OtYhmn%%?l1SSPDtTL2mj*mj&g* zzhXP*BAj@0kQ{pO-y4WGp0qZvF^b}g!1+nxC-(#W(6Q3qFWCIlTHA%M3bUiZSh)8p z)?dttdM^JfvDU}ZP9sG5T!!+NP8Vb zaV3}8$@1!D+h0Hhx9HW7*d!e>qBE=;fH@Ejr#HNZ_V|JlwE?8&KwlG9$rNWIzppX) zZB|szrt?yI)Imu_OWsB8GPBFpt0ED1oLYWYgI!6=ASdc}`ZW(3-CU(WuZL+V44sE` zg`lh(a+61L%5sUl;IK(0E%6g%P$q612J(QQFSyI9xVmL;*fG}s@#c!_ZUleY>B|^R z*lZix!$R6K{Lw(DY(>pC<87wALv+bI9l|<&2d-v#_daQ+1EDpCKWv&# z=))my-SCz$YJ69_t$sjWHWL64&X*Ww&vGu5{f(hw*qFEbyy?Y^*Tgulrku8Zx@)xg zO)}0&T}Z!C9b!`b==ehvpA)xxnWqtc+A=>|&@XxdZ9|K@#6XjB8R)}V-KHDo@^Hx} zt$z5mfh()G&Y)MgYKA}%DjdBfeDAA$gO2_dK$MM3H{7j76)SOJ+fFH_|fGUo_II zJGf8!r7VH85f@-%CvDZJv7n8ZiuvS;= zC#UvhHihZ;g&Dtt#u9D8f%p_Y%K&?Hd%VDOFU4#mmp=dz9k~6 za$j%oTk5W6=ES4QV#})7zY%JafNDwv?g-8z@7}erYvACySZ$e$e5zkLEA%k%CZ2+O z=&MB4R?9wD05epN#*{BvTeD)_XPAgjd!T&5#q#8Uq2t!^Z+`ZesLOd@UX(f&xtX%x zgt<-Clf_vw77@|BenJG4_w+#Ina-qaa~9SRjZJj>M=Oc?4^chDPdh_SSL!*e!b7iZ zo7vL+0G<7n;a6G6=fyuiE0^YOD0CK(Y~Cxc9kN!Fe$RFO?3B8Z6K9q5ND4J7(-c%! zr^q0Y@6rs0b@$-9BI^GDl`eGpLqTh)+^Zf^Xn%EYk4i23oXp#ISb9?VkvE3nz@E?8 zYc8=6`J?@9UY!OSdRGFW3$AK!S=FqDPv8Nkm1H*3EWt}Yum53_MTopaX3j}45|DZQ zonuv0B6#nh18}a@TSq;#EKn}HjfkA0XZo|z7Xyre!>?a;$!Ml(#i?mzV9@Ftb?w{` z1IQ(IEc*p;!Ug*$h(fAJY)EXEku0K1b3@9Ec6Q_J4TKQvE}RzOAonG!+D&&ZLNVeM zjba5GDnEdk^+W|ZfkFN~L_R|=p}sSv-kJ=n+<)rnwYdIE#Ex8*Rd$MWFq{J4Wleqf zH?!axnncLFAZ>tsyuo5c5Q6~WL7UK-*diDofg*?JK@ z-Wh%Y>hf(L7fgIb*bD_V{9OZ$1gnCcIia5g*Qmibdy}Ma3g%DroY-^MS6IM0J(@o) z%x~+~Erx0KF!U3q$R#d zAV|E)DW5{x?oq-H+nUfm5OKpW24Q!>|UTl z4{4!IV&5Hj>aCTqET26t^GJz12A6H!VM2O|bJ?s)71EWn;Lmyq+HsgTws5cQK)XIk zcH)eggy9yluF8y~a*>$>rg5qB<&27SzBgnE6aQTxQUxV)vIxm;mfx2f_l#Ye9nahh z-Yw4ooDH?IlFmAJUE!&h`l|QaOaPK4{!(Fg(w3CVwy!c)K2GhDnx>et3x#-P(mN~d z(12~@ELEhse)pN}vs^g|!3msqmW!A`7%(`5(N`M(@7nMm)y|)J@^#`bNpuREL;A0u z{QL6!%@?{-*I}6%x|_wZ0i0#R3w0dVaO~Y2%108Qmq<(wx|GeYKVxa) z$0{Sb){h$@@KMmtH~>lim={zx%FQ($zjGE;mhQ_}VNmq(b@~*whr!@NS+3UAQ9Ll4g$T7RR`|~)L6Fxvvg&dfUfM% z@pFZ>sw-Z5x|2iIh8tH9>yaTAzNtq~m9_6slZ*N}J5tV?38IEdAz~q`YT(=Xl+P%& z<0!TSc*Yg{-?_4@O0ECtjM9Rrqu)#gxZL!U^@oeNY?p2Hg}3yxS0L7(wsO6lQALq& zml9R&oJV(~-q_T<76_pzkiSNbCi*p_<*BzZkI7qBT7fOQuokL#>t_gkYO+V?ZaUTj zYsrW+-ZiM8#>S`ul8>_aXMYI!0TwS4t5@>2cE!3pl5JfH8fPa({=}D8nq84dz-pN{ z=m;JDV`+r+=#60sP&>O28q16a4GxROBg-Ip+aX%>QTJgeP5sD_3rycpKzQ-Ka{Yyo zgqWZ>_>AM<2;1~0|md{i7a9!sSBJ<-K+s2h}vb@Ag za2x-6E24InFxYy?)96$aQP;N~1C3-ypISidOul@Z9(n1~vX}i^WXx>GQZ4)`kf^7} z5;U=O(vA)a@K=B$b`&Ct_wa*E)V-dC1T{d<&jQy{B)?c2$e zGgIzi5DevUyh+!ig6Rh};XqD3W!zji_SVJ7E;eWh0AZXr9V3h-Kd(0PG5$C=9^|I`2WC8MvD5!y5hMzEnVh6=$%oD~%Xq}}JMARq>f@~6 z1nDah;NuhhxPB@sCp6p{TLdS4Q$Xjzc#2r+mVn?{$^^4|K`bSVm@Oh;0k?1^8C61| zrZ*J_47uty3_FB)McZ0tb8#dN+GljRd?KNUU#@(5t;skE)5o;*(*1}JF;1C%?%+9arJETThhciM*g|cPlu1*9f~JNM zF;bB)n02!Cf}7ZK6nLY&7-U-A&guB!Pv9&_6AfxVe3vo$^cm`*&B$EJu@E0zs4teu z-`(`JBlyK6RZ^1iyVg?*u$?R%aQ8BBB+8i3pj8uWCO6p--?ZFNwFg{XKT);-5aRi7ZNeAId!y)6(nO+UGNNiFIIwp3wpHiouFSzn)kXG zGL`k)Xx%{sRLkG+m25sFIjuO{k|Nz79|k)RLTi>}6wIwG=9B)qyb~A8d*M;CTnnst z|BwA(|AiB=FoL%G!2rQ+@y3PiB(4-lG` ze;RxFwbj5e$SQ`!J;XY}sgwr_CH=@6H%BLEo_ZUsaY_o)ED#}#pNjbt!3#5h$9M^a zy1Kc2Gm}o5S0rDZwx&>4W4o2k;JRiE;z#;Bg05z(Nb4PL5Csx@0ma1~R*(4FACWE0#qbd}o1sjPb*Xaq$*A(-|?EyFz41w{6W{-$Y} zE(u$p%3k5IN(3M+d*}dwVM;Ks*=nZBn`ZeQEK#TsLozt72hH766ym#_{UZ>#!4Bn( zyFy(!2!${4y5M7+LN$$rsA4BfpW)VPVP;;9mGM8%CGMJOQ%Xv-P=1$oa4x@1g4sH$Y!wzW>^UGI^(j@X)68E7zL1)uhJ<4O+>qvJ z>&KswIem_8`y<-c~zhld;hDfu3WcRt)a99aW{%LtZw1T~Wr> zpL(3I_mS#?U$+3K!pIUU1q)hURSDQ*5nOkDc*0@ z7NwA_ql68?2Dw6Zt@UbqyP2;BNG1LPDi4B^^k9QJcU#Jh%Y)C+gn#*l@?L{86ymwj z5XFq`KAtP8!$P6KD0`HP-Gi!9wPmOHFpO9m2}XjsJ(Rb~6yKFfSlb5z(dQpcUvmWG zh`XVX<2p}jQ*G+w2R~(_SVV)rWi*I(m(A_~yAR1Pm33d6e3shmJk>8&LQK5bBIRvo z-GKDL?0&0&5d9{fjPi=2ZW=>G3~wGnWutovX5m{x6U5 zPfHG-GX2H>6gho=iX2#oh7k5V#KULuc>awQ@IZb4}ak2<@l`^x*1YmBPG>itT>Z@;{ zut!Ty$8jEHdtUmCb%tXh!mlHf+c9*%+=1!?YLyM~w> z{7ebS09%cW5O6{VuD7w`ef~D2#IZIc)BPUR>}W~6!57G68+!`mLL_>FD>e^`}u3H^fJ>gA~Go3;*@6ne7$pp;q zXM&#{di-Y3@~0WxxhQ}EFouaxI4u6_1N{=$+6B7xdlhJ<~mNAFkeb=j6fT zqfbrC?MjOte#yaObx@y-c~B7bcpvV?i^Mqx>~%-}4$7Zk>ELG_3AAz#tb5x_#FGRG zFEt~pIx3FaPrU(4Q;%wAP1tN4yuz`w6BP-Wd_^$pIyjW!UJGh=Yr`^i#)g`dp4){NhMoBnN(Sd;?hgiM>O!SZAy zaUOk8Bj2K{yZwRC>Lse&TZ84Z&HwrxYso(bx=pB&_eofqnxNTIXX4BvO&t#4i*UWz zlcme{D6S@FZ1VW)9R!bLnCxUjqglPyUtYx&5``)Vo#Aj$w_F7mEh5L`G43Mk>!MK5 z`2dxBsE?_(2~a5-7>zb;#o4luPn5RJF?TJk0Xda`iHk1bocI9 zZNTF_qn{$QAO8CpL3Y{(`H$t6(nU$rk5E=`z%k-KweDdRIRb&uMou2~Aq)xNN&8va;>X<8>yY$wqrOan*uib$I zJZ(~+qsZsQ1F8B=ALMXjlS9T_4#}toa^Df~rq33}T}#d%rc!S_tlc%cvqd!R5E8G;uL(dI=ciWHs$Ph3b<5E&2M% z3c#bhGafJGF31h!bnx3`4hY4MnK(5)?vpcL)3zD^K!*r z^c4FR9_vb&1DINmt%7>56ZEqR=)WgO#i>*;<>iOAAYEe9Q0W6ujloD zi6R8NKMwD!bE;IuK{d$8%`p_D<-3+$7RLJz<@_Fh0OL*l-xYwS)Es8-HlKAlw1CDhoH3U9)rTV54dff5D>bQo0yZTc_$egjN&tF{ z1%$j@Qe?`FJ3IOQZq|S?@I^a=D=v>A5os z!Ga*VCIa``V6a&3mQ(^qsf{eTWK}AB74z!V&vH`BYq$F)Q!Bl6ZcP7G|8zMJ;=rm<52NwX#2EpYpDA!+JS1ML-IM5hDL`B`v2qW9RuTx)-}+XiEZ0%lBSJ~Hn#0F znAn`44cefwZQE|+#*OX9YOFi$zFX&>bAJXu*LuH4@3RE;t3qk`6nyZY`w-@NN>b=8 z@3D|{$LshJ;$S3)3eRSQ-{EW>`=D1%u0`oPUbr+t4<)k}6Gsd#14yD-%h&{OJ4mfx;(ivh@7%l>@{nU{zWj(ph7WHQC@* zrQlVu0lZD{YvA4x6l7*KJ%^Yj1r@&CldaPiY4iq@aMFAz&Ae<{P_zys(Rs*TWcp`1 zc-;su@QUG;2e9Yphyt6e?Wopmx^&4cM*bP=6Ney_x>)rwFS37i_v4~^UnZ!I(lb_C zKd8x%UHsS}tleZT{y{gOEc22JmKP5|Uj9*e80JWl`0Vc6C2&L2Z-O^I&RM}ii}u{a z^s=nz-rbgN8Z@~4V!6qBN(|UMEpEG7UKgkLnJP%y4f4C$y)2S|{4*^HmT~;BX#Scp z3OLD42#Mvaa&=g4WGm4{hE^r`i%=R8mY961W^`D}lxF?@P~oWtX8SYJbu`q35yKOqc_ZDdTEa@Q*E8G!sNf*K*Dp*62xB5F85XS=H zHGXs~i591sOMH~rJ4&iQQg}f8pyzwKbMW{L^WauOpJo%cWJnMFjWZ>0E5*Zc#LQ$y zZoZOEcv`1cCx7bAG2Neh%Jr#7)V3J#D z&nD=<@8wmit@~i47rF#+QuI~gYcV4xmFz{i{tWn_(>~ikcl#qbOg=r~Oy}^Vw@8<; zH%Fq8?4^S?vMlbvIS8WJTOD2|gwy6mj6Hmo4skFZSOdM9dRp`W(TrbqoVuWOCUpnK zCG`)jkk24oAmnSD;>&i=A&N-xoY6l$BcP9_PH4~qn~8vOIL)+j*h`;{NjjFk^^-k% zl>cI_P5Eu!UL~d#`F{<~Y|LNb(C&=u5NVD7%Lwo85{538vA#gR{=z3r1(fs1Zi8|m zwcF4t_yA#UTVv4L+CQRx))oYsM`akZdsjGNL)=47@P26amQw1CCD1&`xbaE)616kt zKZKGIMwC8HbUP+M&m8cVyyzRmUtfZBS{cI?u;K@xg*8**xVz06<;BT#a$Z~zmnxPt zvE)&J;_cE}Bf`T;ogu8xSTeJX9;T&aZAgpC8qykAdzL3|9dqt{ z+*l$=`e{&>`0mYoKev=JO&QGPo2L_Wjkuk!u|>+<@|#(tjD!!EC0LX-E50HV$~Rie#0Teh3vufc8c;9u})K)hYOe zp2=tYGBwIe!*rx7Yp|+WXz9e-EQ$$}sTMpbnJpUQqhitILXx5wOgq*#h776W93WAmGgRnFKbZ6mmR*%*oSO_JR1iD<%yL`C8% zKAlgFor;A!&S||AJjs)k$WzxpJm7gBi{@P#hFbG>4kaZynUh%F`L_ zA6)y7be&Q*K{@kQM(jVM+D;J6WBJC{f~>RBTz~9%|D^>LoZV|1ZBDBkn9u<@s}6?Z zmMaC#4O&tD)IAB^2P`kNUp!MS?<`H}*4iRr7v-HT0iuO|MaxOtAl9MB%9yk--rUD%AY!-ojSN`aih6I!(>8sbkqK z4PTb-U(uz&t+i)wsP*}JtU6}p)d7wPhV@Z0K9w_0 znSGXb`oBECu^ME0Q@dGDJU4}De*7h`3rz;Ztx5=cz-3=~PYTQOv$rZ$%^ds3vQPQl z>_0cMH6dTQyB`wKvhF8dh2{%8cPtht>qfBEq3Qy_Gn81c+Z>+M8)+fSUh zWsWoy(agKIFL7Z-Ot0r1{MWY4e@!-yK+kfGgcRr~@8TWHu=DY`JD+ z)=}B4Or2#X`0LrC<+Ver4m1vvrT8|V;3qZWM;L=^T%a?`G_1S4g%LIRTcj_<29!8T zam=ChS1Xs-07No$V(K6{;Kqcs(8u+WPZn7#WmUcg=)*LW1Icxl)XNTC*+QRv8|@C9 zA!ac3*a(jBnU@kdM_>7Ao?M?wyvL4%+8A;wdTGp3 z(h4(*y|Z0)(AAomr0ecY%ZfL&Th~@=f1OjYu`pL6tWFF1ES=6IWv2C-HHp?60kr8T zb+#fF>f*GySb_n+FMD0`t0xwrRU@!qkY`XrA#mN{s;6~<{jST*|KgY&#pP=1Ptk

)%;%ZSxT?OX4Cc=&Lr}8hi;07bTGMchZYj_=*zGET9$n^p zY{c?0XQ5Va;Kxu3bkhycca;s6T7Bjr?!b%r6}-Pb)|Ha8BI!J1&sZey6?`5cMJIGR zX`KU{0Ra@bpnyUhLkyF)MAcFwh$~JpS0VsbDE8;avZ;cucfs)fsTqj59(?wvsaQP9 zF$nT=32v_ehpzx1CW&_>37IzK!Qk6A?d#L6mIjVc%TQaF%qUI9_*x*On&lOu{G*`g zWNV7+*rUf78p+@j3DA9^K^}jFpv|{$LI$#R@7-Xie%&S++b{X(1pBM#DXO5;t|5Dm z`ldF84pzh>7gc6Jj;3O6KO}Df64!2AFXGpeb7=%sg8)snU=hvAp&)v18m4p}R6-FT zqJ4$cA-~O53XNvWZg7H}rhJ5)H;$jBTBx}VqLA_OE!7Kh8|3E~pN|^JZe-=g7q^`!)*#0q{s|SKBQTQ( z=oFrwyfS;TM+b)r(Y<@;ZaMo7w`{ve?9}T5vnA+XmcN5|+4V0=J^>;RFQUS?0Fzti zZtL3F-xkm3nY(@-`zc=A%fnK?pk_s^cOV!k>~<=G7r*N<5?sn&jZ8`Nmy=gl3vx|0 zFHe?`E>^{T4|Fq?FJ0JcW@Q0>PW3&kq8KHC!oG48@r5`2u5c$PFplSS{PZ9CAy2dQ z2=g@lXD%H+09N;yPBrpNXtII>S3{!u_Fq7?to1RdvSxzI;|@R}v4?f^K_Y~_qsaFs zCrm}Qo#mR^jB^OmAgMtSwj%sJ666-x4$X!VWMIr#BG{~{x2k4n|0p_c(p|GTZKL@1 z%3G#5Xy6M}%|M~aUH)$^z}a+MzCIEnjazy=gL2|YvfsUItT6 zOv@h``^3&KOhx#9Yz(DU&NGNjUs0t!3S=1#UKf45*DXaTT8a%cw$$z}3K022dKZN8 zmqW;e>oG|ELdN0W3LwkeCfZ)Qj~SQ$5H;0wV;7Aq4@jb(mu;a5Iik7(T7p zdnHQ(hP2vUnr7K?Eq;&)@tZ^|W?$zH_4{Y)>&W2ZL zO8y%o{NLg;eZdCJV1xFBSt6$K`7ca1FDzS-B%o0WC z2F>m%xQw5_ChRtrgrUOuLb$cs;8%7O`q$Q^au58-OpnfzJTpX>Db_V!i>mhX79}=W zgH~bcvM1+`RjC;S{Kc%J0Wpp@Q`~OR(h_KY7kcc#E#Z8LbV9VzztQZm@yi)R#$)D5 zMOG3w*aS2XtQ`*psMjh(#W)p8G27Z}_dgr92;2Q-QWl|^JTK~s0exsLA{R0eBRtuB zw5iye%|%ed(jn|A7G*hmK+%}N+^=n2%hGvZ$Yf3EB&l)DABE)z4Hy)nA>06mD=6=^ zgXHHcl@~#;vJH?@Z%AT}V_~925o%b(*x_vLQ*oa01_BOo>Yx5~|C6IPX%5w*X68OB zP5ODNt@%nOS|+*6*1&zQ!b*(Wm0mM)Xt_;RCv<(o6a|1?wRZD&#voH#sBH4shB|yC z<%TZ$MlvEM?p0@5S^@yeX9sq^C)66D#cB>`W)s2>%yX3M`(Rr;he4$9P3zBM6Z}#0 zvq6>o5^WX^MkB+S#!Zldch3pZTcZ+=IOPstHKCrOqo>Rt0)GWZ4jW4x5e@ot6!F*( za-$cpYcs`3-Kv3YBC`aT!-Hl7*x{=`Mm+c0&7V^}IHvJeJmJA4* zqkQDTz81!+^6@lOo>wqP)bwtTJ~20V*2;|dnV-khR<1jjYg!`0KBu*txIWmZiN7dL z*jnZ&&wD&0cV~HDysYChuWGH*aM(v!Zw?roV#=#D9|XwIi_AEONx6iy4mx2-gwEv> zjdn?P|2RQ2pLTz)zrML4z<5Bx&=FPW@uRgn_L^sN0Z@?4Q>AtO0rDULmk=e?X>MU+ zp~dYBtczuX_TZUe_M{b^g3Fd3(HBrp#bx0rR>YPSgX?c_<13z3Tc>|wu@d=SBke#r zxRYX<+JUJvGl{W)Up>r1P`YvXYB(*~bQZ@YeJ*cp3G7PxJ^D{_w6X`Ts?0gl$3|qN zy}+>yGs%5D7&(DgC3a){pt(76*dF)%8TxQuA!0YuMLb^;IB=J;zuY6yX`IRJff@Z$ zYPIgHGE@yLsD8`8g5RJuE-oE4OObz$|LW5_nG6Q)E+gK{_m?q&Pd@)G=(uRF4}pfU z5Cp?W>L;VFIwC`b{LT^|DGH3GVEf4qQ7}>7PPiTI?S+FVCeDR8{5!u22`r2bEZE#) z2sW-JOb96T6-6?W?&7%a_DE67r1Ii;>@H+azSbkNr-#YuxM8~m+IR(--{${tUx$6t zFrP1u+x3X{+v$K5_Rnt333sAYpDmM=H^BA7lV`=0-u2|7>T&d_%C1E9q#>M=ap& zXql0niDFcfqtIos*a*;Vjj)*9vU(KL2$x)Y2rI{;x7(8qe2Bs$xhloSW1BA260^WCd$iM8mGi6>Nd$ z8E5gX!Ypf(hX<+6%1BQz`nd&*(X|e&IvM(QGah37K9azdPf^fX z;2Qf!)u5_i_HKT8#i%)c&dzERfh@9{xYyTMIwT@G$pEVG$V&?tq}WG8`hZ5~BS-K9 z^ELq7;r-DtK3qBW;IW3i##*3gxi-(jXgLgP{U2W9sw9p-N=EB?aC+KE~Mwnd7;1b!F8Sa5dxrZACG5P~ol% z;avbGBP8aN&PHR1FgMik{N()(IhExn4sd{S+|FweNrDGjeBSHV#kyLrPVYM(2g==cNPQaAOBsNH`%w^N%m+8vo5P3C^jL^7bsL2x7gZu)gt(UX1*+&$ zWG5(KU*uXms+PyhZ*G$@U#VeNzW#8jj0hM|zo6JHSX91fKVd1J!OY3(zRti4S>rl>{bq5B@O$LtMNz#Vd5E$UE& zJNmofDr>Azi+c4MrYoSBO;YJ z6IN|(uHmK9DL6gW?ZfHKkAxYaS3r|{Rs+K_{?`xSVhX3&ty>ep)L2Jh*mD3IJ>2P`7N*|VQL6;JB=>Bzjb~OW*1se0qB(Nc4K8aY6TGq zUT0I~(9d1*B8B3(QDd&sn*FXXT;W0lxt3te_aRq|;h%F#Zkf;2-s4aR_#AxfaB&Ab zqsD+l-17O3xtj9YjPzaBw<%%x-~q4ZPlbF&D=vvf`=-)lFs!PpC>|5TR5Mh0!tu$TQ01MFX=8w$b(lVhTs03i)xruwUcl`0%> zU#SVD!EHvW4i{Lk;pUs8CgeW41a~7>A~c}&2tCSBXigQ4gfvmy8Tzn&3%~7|C+PV( z`O{C3kCc$Ka29o=e2=73J4uL_`O%6PPEi zPcCu3Q>Tqi#NpRj21mi8u)^#^_u5XZaTLD9Ji-HbEobE*gM~`7ONa~KCkNC@nh*ZUrG(25QhJOL2%#Hk~E?#5lB595q%Py^63F|RG(T?Odf#6mql0PJ!s4! zen{?kb@4Ni&ZNoY;+Y>3k<|jiXErnOQR`5Bb7z@V6(W+U8gbxaIA2>boc+-UOT>WV zoCxE}Jm^$GCw0}?^U5EEOTdgl%s;hF4cbqQPr-)mGap8BA?ntOc0cKBW=k7Qz({Du z=V2CXZ?~c|TD2sHa`Nhi;XUQ)w!6-ng!jJWb0IdUq#rwKRkH%{l&_zDg_(q7_Fh@rV5_6hD zhHGQB@yUwM@;-G1aO>vx34udVpjj~xTVLGR!egbTpPGtq&QhF?c&4Dl@Na=!z%!i8 zrCeNm%cKwzr5e?AO9PL|hIc_A*Pfuxcv?54n83vJA}WJuX_ zR}Y0q5^VNs3$X;8LKltRs(c(AgP0|O>@|I9l8aht=0c8Bxua$zyh5D^sqvv}`3>zG z9>i~S@#t5vsH_v;BBKFkq0lW*mnwPy1kuvqK;8}{B*&F}zcvQC>^*6w7x!zs@)NS`eeL|)G% z51S$l->DUaKgVlaQj!{eJ_DO?d%w2^kg4tzdp$Lix1zBPYTJf|m2idjz{%8G71h^z zy#c{XNeDulQv!@1*LZNTKcg@Hbf0X^tSGfTdz0~dNdYktl~Eg0t=*!?MpKdu1N8Eq zV)*->`a6y`l8i7--AWPK1E8A_Q#+---T9Qimf?p{9$GWywAhntCl@RIZ5y%I?(E~9 zT#(qZbY5t>;liIXD@az&b(+o*L)^!w;pMN`RVv_QZprD)Z+kY(HeMM>!>8E1n8Q-; zM{7I{n*i5?s}0&~vQ=b~F@n|g+Z5|6Ez<)mDf8|5lc+B)CRJ%CRCbuT?B1`J!zWP( z(l=XZkgZ`5_pn++1gReNpcSDgU@j`Q-X7?;EQy}Yy?OSx$!Iu(Ev;3Q)2m}>-Xhl4 zE_(G?@QU#y$y{~M@r%0J&M{es@hX0Fid|lHf$`sG|D8rUjaygTdc3P9I}$XRADeVF zk85?W@v-u_=C^*&?T74s;+87siff>!=HyVhsFNDSyS3_7&}h59GBh<2{$Qm^YB#)8}Kjto{ z@UEY==otCcdyss>;9B$WeVnufcep`>>)iV5TY*J>I?X!GR)37atE;VZOS*CVnI9V8 z=wb8<_rw<6Mj}03&Zn%vOsRmUAApCXzQ!j{H>QGlHvGhyE~ocTrCa2d(4B{06`wm4 zqjeZTli&3k;spl(@@dI2&)T0t&LJ*G5CE5cu!!FonC}l zRH0N5P2QJ)EEr$$Zc3P7FJ#2KPDMCNjcpYs)CA+a$t-fo(qY7 zXupU}X*3prB8Zr&UNmAjR3cA$(U8!ejoPOpd->=EnD`Y&%>{ZK2 zNEknm+{pp(?FCk~ z*an_(YK(fWaAZN)2#-#-vnX;(SVeD=4G>7MtzblDo(y=@S*uJ$Sl&Q>{rez`D-eUb zSZ?Rz0-=vi)EwUW{)Dt=7PBo{+F!ky-cKtV&n6cH+;EODKLNpD>iHi6^@?mH!8LW`L&{B$p>sOgQ6U7ivE%OazBW%vk*TVxVf z2lDI^7|<%ezZI(Zu-+(yCA}p5Cf6d6>_&~^;JY+}<(BY{=X~oo@Y`bdt7gRqG-?!) zEbK7yrAys(+NI2EU=}?ae-l?&Gw~#JRnE+l(b>YH?bDYidT@iwOT<2Ii1dO55U&-N zIh(;MVkzLJE#x7&@&OhK68ayb+oK`ymJvUf}6t>?pr2mHK1^vKLduNiZQgLQSY z9F}El3Dc|^bXd9Wl7pYPFHME|KDqA7Lx}wTJMiR}hz4`EdGVPw-X8R?!|vZe)qi~| z5!nWz4Ce|tsTY%myXagUC__P1KdPv<1(U&eN>0iO3cD?^SZY?|z*`qvWP)tN4sR$1 zh9d>lS;rjOFw*k&w$#^eG(DxO%zys!M}3&XI@!i}uRrU+E8A80<+FP(Vl(`QxQP+3uG`_nGL3Mo_nct*yo; z67t*PAVqP#@44+5k~Ny$sgW$0f}`|@>@2VPo8Y8|j< zruZg=5J!k#0U^7Pw$8ss{((|ovS41n*!mQM2#@CH7)RkcPeU=#HtwX?TqbMqhy_)S zC93|--lP-$qH0P8KVR9WI*35m9#whSEQn8+_1pQ{%zG}}`MuBV*stSlXjT??u-h?5 z{;+g6*jY|hlv1OKG>WHQ5~{f)mpLdvASNF|_f1A?zrThd5av*`XynG5&i4^S?!s34 z3FGS%3V3J=q_Zd6++&tvn*?9<(#ebV@USWv7DdKePh7l59PFk$3gTUs(lcj(IY5C^ z$Lg>=J?^AHp6(+?^HqnB#B3urTTGjUmgT5CH?Fs` ztfNhaIm=}*GCX_mzMn*Yxasn1Qjv<$T`rGY&<^!RLjH(Wsg&vz%oMeGp6%?HSoU_( z3=wm4?Q!bPd}UrnMampy*>W+epZytXp`G?Eq)!X}`$-lTa>GJIL!A&jRJNh@M{Z||IHV?~F6|R)LFSF7GAd#MwVlkPy9;gDZHcpxdy zI#QjR9xhSWixbn1bUZzkmd;tIl^baLtcak1Pb3=FhW2(ObO((M{7pwxIWzB(F*2y8 z9(n-Lv9IJreMgELZ%Zizrqa!BBq?o+VMvyr+az~Ye)GAJ-Iv8S!2@0ce8r{7aahyp z71OX61ASTjK;_Pk!;&ATT!<~;0V=UpSY8^eRf3tltzV{fsa?1LdT$$Ri#FQZRK_

2v__ujBEMHf{2gAdr!QOohm3}(jQ<0m`hS6~E@MRBVz)J=g3o54 zsFlZ07}a(=bg>hNpOwV6S#k;K1$G@%H2kkOh{pDy1e(6;#bB1P>LGpeMoqN*Fb$O3 z1wV=2K-u9vAAiIXX7n=&sTpGrS%20fjGG9@ZZ6jBRN3NSCK{TZp~L(o*y&AS#P3P){Hi* zrr%NmEW{tbhFA#IkXkhM(E%9Zw;A>?Y2nXgTq%9a^PSG$TWeHV{T4PPGvmbYR$~3e z)DEGXkBEP{9E*XmCEgkeZ+MvGzz#XO)0O40pv>G*z_a}1&~=x+0-L>+2S${d+BCHn zrIqo>)iL|PTsW)%<_i0Ks2KyjLXZmn1Ie7I8v6I>0Bq!4XCby`^{$D7LMay6H{N)E z$ZNjgs||&rElvo>{96l9+SJa2%BP{r8pYHr8#Ta$*MBRPdhfbjf z*|%hLHSgLJt#pA%P7sL5&C$T*1^3vf{ghiwvVb$~3Cxs?%hucM zxQ=e#>p-b~tYzeFYOfwlAJTGGUnhaU9$JvDuYD00^6@>3{b zpn1wl{GX-dHfN;B-VppR@iVy9^^wh}z8b&@^^?eD1v91?pavLOcIH^CJoavwXl=A81ed8j?dp7juBrqxA= zolRB`W`z~oO67S%1rb5|d5I$&<4{~&(ekj+I@V^p7o}`f#1GC^Qj1d_DSdyD8q3bg zEOce_&`JK^No&X!Tjwxs+WAhS+f4IZ7E?N!WCL*(W7;fV8- zns>i4Pt_tnzr4)O3_4xC+x6(8tz2#b=%0 z$b~RuevRDza`P+MbIg5D9vq+MlOb6l#yS{4)d3PoArVukDxo8?#-4j7`QVaJ9ecg+ z^Mx#RUH3IDox>~S8b40lfIB)p7Znj;N@MUxEA8l9(L(OImR)GMLi~;S(MMb zw9P`3m|z;`^ZQN`uw&U5W4B@Bm%Eb!=EDn9cty$QHs9Gt-ogXmlb6K_vH19)@a+<|CKfP|h=M2C_bF86rfKTC34am4VF+7 zr4>BjgFtDj%oi!0kAc;-7>5g<-lSP5*lV6-j!L4D-N32O)8)h3#$?*eHa>nKT=dTl za3=tm3uvLd9wf~wzHMSheRS9>8bkAk8=?Q$G~NxiOZLak8VIs~Z+{3pb>ca4=MQ#0 zD7p#YL${u*A>QaP@VxBrIEAt8G1#AJCCf5Du|l2vCgwrelf3HJb3aZ&cB4DT^k@v@ zA2JBn$Dds&7u?tAt#74{bE?;l za_Iw;Jq_$|1h^_3?(BV+p4${i8Bu1VEb}*dBmugjO~J#3gVfb>rGhhuiW3>3|09Xt zBcpWy6Xwqm6`m(&IMM`_D8;_4eHsHvIsG$szVTLiwa}aMaKPDovyB1<%Z~$8;l4{2 zfYd=zYx1(bsTSwIioE27<9es^{G~=Z_4{~1qE-#_7QT)4HcUss{*#<*;}^Vb(HhBA zj-sPKTQz7XvD$ur{-Cb^D71>hBYm3m(aCWP;N4Y zPIlOFJm3w3^ZLSi-1B{z)enUJ<&cj!c+a_CE^b41x}YnVI`k$b&(2H>_gx|tr!BvI zSkHw1CX#RA7Vk2BSe3evsh7!p%L|;&r6s$G$t+ZdFuWyLzrBi?L7n5kn4P(yV>e=y zV1XvUS2pex_-4UK1W3ktJRW287*4J~pNh-XYp|m}=~+wvh#;iK86bDLS(cGml5Jne z4V}@MzXO84#jGHaD8#GyKoiKHW~}Zdz=mh8?ADB1-qT2GhiOr!&=fk&Fg)M53OHw)Hzm zu&tp>_k))N@wMB9LB4s4OWoSMtCP6b^ZU5 z_ErILc1xRf105{51b2c5cZWc50t6a&3l70ug9Quj?k>Td-~k$UcW5*Ow`ub3z5hM` z%y%?rbn|cktgcmc*L7DNA%a0j4e{Vp)qLctDsTVmRwH_B+7!{)<^-F!u+Vt*KlTWI zyoQmfw!w>?jQa2T^uO5&J$NV?QF~?%xY?k2zrfjK-KXu!DZ!KE`MSVf2-aR{fAlah z=e@&f+ZP>;&nqoX%8*R}tdPl;p7VSL0oRi6d`Jt*b@_Uu^Jl=`sAJxpjO#L6{rpZD zE9-8tH}8B0816_8aVVcIC(2W2--1pg_HTNUm8N91$+R&MyrJSY3j!Wq3Ctw78*!hFLN|&II+M7N#>4npJf-#7_1^8P%yR~VL*FUNh|Tg zFf!0f*G9r%W;5^VmHgKsy!X)d6k^YIl)x8~Z)-a#CbW7obVcrNkzbOPc8IC#NY62iaLgmDitSkkefNV1bIqhE{5~Ff z#QQ6I*BX)0yM4Rq@YPfPtf>AmaSd#Yn}giykNtoM=UP@WmLl_rnvN7V8;fz1I^s(y z(&vim>&d3fsBs+@cQ;c()*yTKev~z(EujhV>&Om_dmLcaN;`PBpU<~^MIC0SQsJ;* z=J$T>FO|ObwJ$<|YNG;#px2x~H4=dHghTez(noe|V&U75#ihJ5f8a(3eb~c% zmm5xwzOQF4x+_N5odrAYX#+yB^}l?n>a`LP98>Y}p1n8&b!pWWXqdz2^rHXm57y}1 z9bFyAJmdXZb#vUHZG~$A#rVsSdR(+{w0l+Z7-H@nw;^<*0qm5?JZ{!E^*l)k=idiy zO-B&Oiq{**Ij=2A?Bz!u;aOb^i(M}1fS)YH2V9i8oxg+t$g_;;B;1g*i=ca1V6v$* z>Agrr4C$eYaHvz>ylCg^P$7U*XjOUo86V{=!|8+_LI9)MiiXa}(h*0TOBR*Fn;(PK6%_l(?cW z&)`lQ%WlxG3b4sXhzQ z=>oYNMKj5}8r9iCy^FZ4_`?MhG~s}C-UWDH(e{V`u;(=HOv-OaVU$HLS4vB~BO)xy zC4cn=K!V-CN6uA4snA_BCgukYx$0~g#5km_-b6)#dhm+!~|1%AD5djmj# zS@)?|JxP-eGnR6T?yl9k_2HVa=qG3uA(`=fdLxE|*bH~!BbLHy?o<@lG}B5w0}@A+y($HBt|10x?OK@B8D z>obABDs=|&zN%az1Kb=d*n7Tf^*?v*%-+r)O4_X?2eBiojdZBj_$O=`w(CmD{+$5N8&2*80A# zAge=NVcNy!;>F(3P&*!sFNY;u)vVJ`-)jXeuU!0%Vpy7PILQjmn0?`j(u&`F^gp(i zevR&-z3MR#>d!nquSQ-$(Ak0J&QDB8m8g(71)KNi@SAjjZ-~v&0W2YaR^+Tii0-7mXl&?$7jJd_8#-md$@7j)J$E$zY_U_owc|iUk00x(06@-7QNxB({t&K z_8=tIwGTl8p@rxvH7wnv$BcM( z3oc3yi}7|hvP>W>j7@6lp+rRgT8T+|J?%ZV*DEe`&eL*{zs(s0~8b5PIkoHpE734@kB_{?PCZx z4zH@6uUJ6?+{1QL6?`e+VYc96J=t{>LFhq;e;$P))5uAYsdd65-U9%m>-t^4tW$f8 zn`rLMJ?jopFRZ)toX;!oG_m(MPon=xfM03-3d{nh~`(DYmqh4DLSxIvk)M zS;YD+EVfNEs`yn6P85uGA56&|H&qd;BF%4)S|)Legg<)3QrH02Q65o5In@Hr+~#SN zz6PG_p#q-E4tAe^7`Fv9^;0ve&3mFDdUE|le5H5VR5Tf^Q?+;NpPm753Mw>Lzk@KG zmIJc$wlG`F7di)BxI(%WfTHHqMzyuk)+;e~`p-RW=&r|4dKgs%+ zW2g`-)%=&#wcTIn?YKV_bYM$3{(A~vPSu&L)Ow3?w0B;vTXcwhF%$q%_C_Eq!m+t0 z3F-|SE$yZBlVZ#sk^OB8SpuFail(bT64yOOS@#A~@@SCq{MH84sD7EGH6*Yu1Ou35 zAiGEo5qQ6^G&@?DXkWvcx+EFe$0ojz1j0n?*M)nH-!#8$lGDt?Ql!U^B~!4Q|6vwC z&3~Ze3;G8ZaFR3DzCljRS5?D6dMYL`yOA&5Vea>us>vc@%^n*Jo=Mei7{LA3;EcJ2 zXBp4it=z3Cf+yC@XHsV)Gx(1}V%+=*SMdhHpwy1?vY*6X1uEmh*}cQ&i7UU(j?UeB zz{8~7JU_iB;#?7An#`A1YF^jSm-J%^vs{~Xx^R>^y5H_XE`Lp54t3BPXq}oGndDPq zRi9tyexx%HH_!s3Nw81m*ODJEBAzj44o6m+>#mx0j$0ue9>M8c;*c|E-`bM}#GNaZq3V zaXZq%m>-I~Xi?#%b^Gr-v^#q!(cQ{GAwEm}9?sut;-HK>9|Ei8%JhVZ%3HkaW z9+#`>T+{XpoOOeU5^+<9>@wP4+jWBl21~S5SO{l0h#OS<6Sr#4un#e2_;?cB_?9nq zIbhRV+}DHj({vU!K3lcgN1c4N!}q>ISWv|!ws%bl168U(2+CPLfn>z6FChrys_Tj5 ze$3%LpKT7H(=Mis)DaNKIfi29-ZC;>q~67wnRJSLM<1>HX3*BPi)z`AACX%qpqHgE z{Xvd&W0{Tt53D^4!F|PDi;Mu{#Z{4J?%-Q#lkpl+K0R7~9_p~hFeJg4EzMZ$Rt^P^ z9@TzTepwdQgjYeAq$5Lh zmM)G=g!YxfTMgB~4bOO_kkDR8=J_%T{h_(!UNQI2%lDn3HE`unNsLx{mPq2 zC^*EBwDOKg*0JXarwiY$sEm^qt39LUUa<1&$7lKUqMW;2~Li8U&xQ!U2X;)XOy~UCG5`3lsZ7|^TxV&%s#s_0j zMj|uVpfy|8iZeJvKaIUT6E()iYYY!%IdF5B28S{Of|UILP%w%^J}SbbPSaGZ!O%z! z53rnXOOP6Uv73-bi1s#uGAAH86u7&1V;N$hYiM3bw4>pN z`o|<~vVJ=p86}%`Husfy*Emb7@8=c03VGTLe-e!|+N}fafc7D~-Ox2DR1O0yUfUPr zJg}lhwO{hcUM=~!zC)rA&6SX{P8+ClGkw~1uh?gF*`zyfccTJ~;x>?i*i31J25@9D z-B`Rsd{309)CUA;kuttr2#?%9rMr|rp6MtHcYgZL@ZkMCySRq*7vH6`Vr)Cb-k_KG zA*0;&F;yz)REDVEE^nTws#58-DU%mFD|QnUE%zvEcpc@828tt%aLXQ1pjuTgKY{av z8Gn%)Lw|ci_-7j%vN(KPT9J>Yc50K(aSE0TMD0&$^{zK{zU_r{UY_>gWJsH4nFzl{ zc2{5TceyfBGZp7|Rc#h%rnH=>88h(>xG+O^P%kJ&TwWOF%aLYn!UFJ?3^?j7q+vek zmYm$ii)v=fg{6T>U`X}ejgj!ltoQJ4p9Tj-c4I}Pd({zibGxBCk$l7LEDN#=z)dv_ zj@zqmIWiUcdv4AsKR}mB>fa1~2QK0Hpyea{R~w;C;|1ZxgN#Ub{^#=j{{bOquuzeB zD7OxA{fI_)52_O5j}DJ^o^pIQt!mUkKrDBvCWx2Knw`2ffNrBVKoDY7vM5q*N)mFo9}N@Rte}WNP3XFzAmHNfHv;+FihoIrfpLU*UVY zwaNl5bC_s4HB6l0RA1ZUD@tV5&x^Up1oPKniIkAeo25X8!n<7fb|{*|fiQ;IRfiY@ z|3n6zu}tIXK`c)caQ@f6!s|Z}>CJ;Ncf*b}6UwN4Ii+qX*@TV34ro#t-6tmg0F6ux zuGnqVpbK~`a##s1=@{wG+E=O#jf}Lu}3ZL?ILay;+Dqt1w_fAPTLO=Bs z6ms^3QBF}ig;_76z%Be})pFa;%N@uaX#(?n`T|$bFlN@P>d-b41EP1#iQQw65D#)Z zokH?|oAqYe4+yloe{9?f!sg29>g`^+hwUVYhoAriQ z%$Jb5-tH8bq3R46^-TJ2E^_Nbm&>^3ONdbX&be;QJ}HbvfP3*1VRPFSv$mZ_@j!{^ zr%X@XV-?u9%jb932|V3f94Qt+6N{2cxGk?Z9=c>2S&5$=v))ux83;jgy{*SATq_Tv zs?fEbx&xa)mWN1MJ%WRvVwvQAi0{&hYg>bHeqd+c70^#XvVNLuPv~QD1ACa&UN#G* zm+K;{51#8$Ut4`B;s>zAVm3R|n4Vk4$=e6Q4}B1Y*l`xYj+$##c3AC5jLV7cN*I;Q!AxAfLN)0$>d2ym}WIg5tPke@7 zYqhuqY-KPOQm}m9T67!7m=(7X@09k zHVZB(0ye{hpjTIMd@WRtdDJMc-Q7d1r2%q^BDEzgYlJS{S{a zIl=q|S%S=5oXY6m0y1>|BpBFP@587L2fIb*K6_m4gZfv>6}xM9b6_{t)StAcM#%PYF+chbXRd@!khGHwzUI724$#F)J-Nksg5M|W1m z>9@Obbd!}{1Ekav9_*V7iHHRWT5+uJ$t7CYth?Ux)(99s2{$NDDADEBGLT~@86k`W zKjM#R)6xOOvbVWa+n!urQ6uN#-rsvn?B(s$@^CQtGfMpMAPsG^O!U2buLbVlU@nA~ zbmEkPDMO%mhHEGA{WM#8Z>TOzdn6Y|5Muy=eZ?>=wC;_ZFMcVT?%Vli*fbI2J^t>Sv(p-%zse?Y(cQ zZ7@>g_ZCb@n3}ML5G`9nrEwOzD4CI|5#T{z^e(Krg_Yj0m13NWt-BUl));b?@ppv^ zmlG^xNx`zIF8mdEAViBSQ%;T+I%&(52HKsHLD=r6?{i;!7dWH+ew#jHnp#eY-O8yv z=3c$sU0&Q9m22!B1vp&Q#}ThnX^Og}2N+=4z=U;(h5GZ4MRTqEQM2Akv=?Q@ z9xmJd{TU_?YIvX~N0!{2=z4_ybQtc)0uOq;zpbu$kHplMm5q?HVQOa`=UUy>TD&9I zhgRnIuPXd*s_61vFHJPhW&eG=URv*1Kf5SoPqdbbgNBzQal-efF!Ww+cz3qflW0eOod=U;zYbS*bWru0_pO4WLU4I2VBB&0~gd+AkZQ`EuHbsxlV!4w-ZK%-v&m&i& zW3F-}{w(}UGA2IsLl&@e-?@$^p^!ZNdCHk7Gwr>Vuhc_SBAW&m=NBaJwObq9KOReB z8qOpTDZp7x_5092+FWtoA6023f3pdrAHxSgB*o=~{rI&wP@b);Ncu*#=K$T`DYU;M z1&BkphD~_iYQJGPLz3M%Mm$SBmKeW)MssNaFH<5^2>Img2i;tGKRf6p#Rr9)V99B$ zgmnl@{z$q<-FQM_ykZo@Kg?=iy~D-XdcSIedydPOeqf5zN{Ejg0gV!x)Nf`$P@|+U z2Y$xhJ3l8_jNg1uie9#{P!^v$C5yH4;a-wZWe&Hb2fXJE*hf|XGiMI^yvO?5;<(Pr zH~+D)#&jv2clhw=XSL}ilG@JUsRB@B2Ib;M*n^eJigUR>Ks0h)-VZX_!p0a?VFp}k z*1y@jmyDI?ZpIZHf3cuMc`tnUTVA#`{ms36e4u#8*DSb$Zbr6283Mbw^2n)J^43OZ58Qe>4;Y~8PJdO6;K-7 zLz55(fzMxKlE!eOfas)kf;x$l??G(l&wi=}B9<72x(%+B>Oj-g@&g+4=V&!?%fMG( z?G<$BIyF!znrz#TlLj{#f65$iMbS@1Bv$~IhOA|L`T%`Uf}9=}^9dR$9` z&qH{hTHGO9qFoTLUoK;OjE7IE#T@_D{z#B&eo}0{Xy2$SLaMDg<8UDcT z9&4#$_wy${u4*~aMl0isZe>_oY&Z8x zsxN|)7_Oxh^pu>woKkkR1mPf_C$uBKSio-54s&}s{Pvl0s)_l|nlQ8A>p9UzxEc^X zKk4>O`&5+v2(f`$tJI)DFZH$J2@dcMK|pD}@F(_JZ%FR@bL zRS)t(Mfs>(xzvu#OaJBFV-0^HzYpUXr`eAL?fSw;4I>D#9OK3OU&$c(L(v$9EWYVG z3c?;Vd?j;}(=dJ!iJ%t&7S*J}T(IEh{>gzmWoi5Q;k}Gz+c1e*ET-U=`#6!5yW>Ab zM#Zx_1i*5(@SnjAw`GS1VVu^Wcz8I;4Fb^^^+q&QYIk(JwLeyYr%J$^$kH_0J{QjE znE&|m(Kdrvl5?LlF6=ZjD#aVg4UXGbOR|<`t9*5(LVA#N#VrSKC`MTiJF_TZCyM!{ z5orrJ%q}xT3>`=yt$;HTe*)l5dTH94qEW;8HnU{c)-9Jlv^h4}`vLlU(cJHlMT3yi z=4j4YmJ_jea$uvBD!po#HD_s0=bq)U?v2NWjY=hI(J612)izrP;Cfy`9TbPwxhmDi zahL}^@Kih{=t5sKroAPP6xvSsZt+jwdNB{1E0N7P#mThU&~KQe^sFmDafMr6JH6%^ zw0|{ed-l}jeLBiGRrrd_rao#_;{o(44*GjQHVUd$6a>A0`3CLFggADC0;<#JSwH)V zMPp*!k);QLpCvxOS<&xuWAH-$LS1gQdgz-E{iAz6XhCG#dn&JRykjdx_$HEFpgSj6 zzny*(=l?{fSTzUr_}g5jsPKS;yjBN?_O9Rq7`c85a)(84tl2}yNHv)5r}?We!<5gp z{NUsD_OOuhZX$BD3q<~1=k{fbBk-?38@gp0n&TII4=4Q!BsppPymU%VElm5`$FW&# z6hAO@U$W0&q!qjCCqv-~2<$+3Z}p*C=IExKjDnIrv5)s(Cwcn zX#4*b1(TQ_=wLJFK4-=}dEM-&kM4H1xyzY|8nO`_wcd)s zyM+JsW3H}Gd0Un&gG(zNyim)MD92UQz_ao4Tk~bNsg0c&F*BvU-wF)|-vwVZYa&x1 ztjwyuBSeIR&Afs0g_baY-rFZ%Dw0CeS{`Zwk;ZrYvv!DGv~gJD1r|d0mDJpjS-NiI zQPyZf&#!Hd&eV65*nzk`aN2LB(e38X;QJUI$CJpGe{~x}(TiH$C0QYN?8+&pxC9nT z+_{Epck}cr!}EHRBGQ62dSyRDlEIXmmR@z)8i3W1h#S zS;Q>&hhEgAK7U9Ci*b6k-R5)@#(f5eF(lmw`->l!Vd5h-ZsG?g2@HPlGm-clcL|BE zchmC|Sn|E&50{Cd_Ba4b$?Aph#2a32DCI(+YE3bhDB|cV8Ue*HMmu09Uyt zR2VNtC~8<6rPDLK=66Ow$qNVL^2%=sDv^`db}I$`Bbw&pO=--$0z7tsz=2P;Q`^_s zNUzI;i$3t5TzMnE8fbC1g27M?+%9p3?SaO&r&oX~%h{UA35Q=W=3Y^_^Q(XtqtQ_a zjgu1Szo^P=Q>db=*=t$9A{O|4Y`9WN_Y0~>+T*4#tzRRz(z$%BD0Q`F^)a@lZq3t* zOiJ3Uvmy<4GtSX64+dL#P$Mdm`CB2+;tRV`!qE)wTN<&=2xrChPRd)C!g9BHc-?^~HbBPMHUq4O#Ldm;p}-^6sq#@%Rf9gBM~`#TVU{gMt`%O&(v0&%JYt9| zqP_SfKS}AqLW_h##T7?zxs-joS@H}=Abvp!A-m{13lK`2PaIADZ$~aF&ks7D8!k`` zPQ{$785=ME;)i_VoWx(t7ga8=>*se4V^RENa*2{J4htWV21is>$y~vw#EsM7chbC{ zxFYUj=UMn?&2Q6~O}5|_c+kHY*KPB12%gQ(TxkVO{fOY>*LR2jfu(ohRH$TlX8Dj# z75Xeb-ROV;0SJhO>cDx^dsnTcv5Q{sGcMq|-CcI??>T3t{^+^KweCPTF&M>Iv+C5M z22)6PMXV@ofUpGeUMbNGvkkXmR)hLq7E7os63 zRTuC$|JTBI`hyY1@aIJe_8W>@|A)7ZNd-*{>Z2#~f6V0gZmGkZ;SK4IJNDuonf-&g4Ep(r?iPxG&d4Wq^%SF&k zRZnNBHsf!r!0nt$lxOZf6rtnB+o#!`4(efOBuLnDxA<#kh;)zD`{(kNsZOx^ua?7q zLvYZv&z}&MF^QQU7s-bY9;;Uyfu-|{ZJ_M36!dc-g5)(Ve;lwrS8AA<-dhRLC@1bl z#?mn=K>$96#M5QMcZtA_8^PY-`5Dn5@=&?apniR#g`HSUpO4|`(a^K z!`u};d3aTh-2mQ{TOx17vJi|7&&gnAy!e$;@P>!8Smg_V*Fos?tCS$p8|ot!(~@T| z-_Vpp7Tv}w+-)QjWDf95K^X{dZ`*x_a?Pg^9JA+(O++VNjBXv11B$@-sRnBSa!b}+ zX_c;Pv&iBsRW7bDhw6L2mlRScow=I%QmJA<#)Av4SsA#f2tA>nWqQ!TeUp#ERZpm%zI zFvvw%`V;9}j~ffMHI1J!lTah~fG z>8m`r&TqfDb#C=iXNW}uoFWR%t1{=8YPg`CPpp^mNX(jbDd=i%1@>eC{Hc5c|46=1 z0+#WcjK};}UqpF>!H#rlV}Df(9HK~Ps0a5^y2~;Cr8AbTp*|MLPeXP}+HFTrRIY1mk z5hZ!v%s`vkqvZn~+V7;P8OskMG(!aP8J1iS-@Q#1}sGZQ@ip_*RN?xciRA=c!A* zgJwiUjo>od#fxL^^b>_R)nDeJQBA)LjG8tC4uQY-9)={#0F%z+xT69b=u)}@Elbkh zIlGV^RegfB6R5eV&Nn~#*|1WjngD8kK>1#1ukuOpH^Gj4tHILghx~7k+6WtP``wri z%W!@n(@~YHAeolYS>?+kpDLopqO#;)Cb~9I>NxzpHm^)s6{0-pJ~sWY`+Kj|TPj+% zw|>yp;YhMc>XRor9#piz!gx-)#+JzlW7nRYpaEDi4=jF?9R>Ju_e@~i$ zAk@SFszm)yBkTVtXrW`!blUb;Le~+Drg3|@5TFZ0)&h{B4TcixLwm#4%TfZL;+}fT z=o(f-*2T}Ppa4;(D-Pn&5I;OD41`=<9CS0+6q>k7qa-DO+6=sy-tm4qHtpx~iLmN$ zL`q^j9s~AjIU*iKhD50oK+r))*$afoROa7Mz*(I6QsHPtyL;nop_W#NO^wFn`q+H9 z@~3MEX!%KW>P7|P|6Q&l^N+>Y;fS-rLLB&{Jp{)Q-h&!9^$pP#Ry=qHdpe>6aMIp5 z^{%rs9OMlVdAbC%j)a- zb-fp?H^v2PB-1s0MP;3hEnWtKUYdOPQ{K!mo~s|m_GHy_l?Kr@m;On{dw)Yr@T5r= zM|<=TO?&pF*e_6gqm~3_E7FovCbXK;Ad$_B3LyuRf-&N5`-=jYG}Gk4XsxxvBf1-V zXRMaZz4OU@bJ6LY6^oMJ(5buz%J56BUVNdqlvd`SM?6jfs*@!?8n~xd`I_Y*PzQ^R zx!=~GTJo*8JC6dtQ(iixkYgcu`;jZtnjY^g?i^ zt?es~AFr=mAN;Bbv?;3gw^wESk|M^09s*TNsgNxA{#0sb*eZ(dM2$qo29-OE{7mH1 zR`!dNtRFQqC}YD^6POl&Cb+Btc*RJxAQoJt_T+5tiWP>zh(O4^I?`_eMpI_xOD1e% zV?2=!OnTo+0&+JKV8YXjwY2!dqh0kd;?idC2c5s1TWLoB-KIc1+X}~zCu2sNH^dOe zHt5<*5Wp>})6V_l#PWBz*z(8fHA&jWAb^#G2kZ?(KEvYyfwymDKuB8S8-V|O-~l3~ zASUKb`j3ix)p>myL-Eo{yuLXlEEpxxArPSO)wNo>$#g zQ18|>a`d1?Z9Xm(JEz}u#KYPcg1SzX8BJ$mJxZ0%c7iPGph*T1O)Z($={n}v5jTpW z<$hAG4H@I$rC-oMX|ThN#8s*;9+E6CCPJB|m`%hb zMi@GA@P4;0_vul*-9fJGn6`9} z5!Dly#*W?Mk-qBa()s#ISXjBW9|Faa63Cwrl1^S`HkDfv%z%u&b5@71XM{HwtHs?! zV-5Zot!go-z7IHomZ^?n#QG_!$Ceq5cil(8IqXU12Rjk`nV-#{Q-3-f!P`K)h<$Ol zKCGFWm!(4O*ZuXh({g5TA2W12qFU4-u^f!Y$E6?1Ol^R1F*+E zzkylp6RV@jPL|2~1RbbA@ZGZan}(;4+ldrHO^WD2SRNDjo*kspHmm>IPXyuY3Iq8} zTY`Mo+hK3jcWhzSQj{0PkDGkGAUz)lAXSswA(&qS`%gpdi5+30w8b7*u`SM$XHuiA z?&#LdP{B0@we#_Kcr^O4B2P0f!^J${*b`l`w12AheQoZ4&>}s<7uQSZ5HnZo89Ar7JuO(!P=5+1IAqvMGG_1+SucM=xb+qx{0c(JUU?0)`b02p>q_MlW27gt?lVWEM$s+<85 z3|F4bPg~)(3oB2+Ge?!2X#M$*lhPDHs$HZqb**`e8 z>XPS47`8=*7WXl_>B04`hYicS?6RL~99>Vq2X2}4eo-^e_UovuPbmViyhk9TCt>U% zzUGm#ua68(F4#{8;xy@GSs`fDGM^cgS07`j9X^8%``tBhyu7O#fZ5-SQChIPesE8J z2=t>S2pE}iB`kc$(2yeMHal#iJmI6<#@#5^#tSCUEbHhXtfmR$G^37Uo%;1F@$zvb zN?~dp3JX4sHCE^?IHP%ux32P(t#Rav-DF$S0dZiFVZL_xa=?Zg+^-4Vm7YHH0dcyo zL_*qI>cmAUvM~k&{3z$m#N=kw5dZbG-{1F@{{6BmkJw)>ivPjh=r~~2R@fy;m&TmT zGaZAKnN{A?LK)vf+Vi_dnJp8YI5IXa&#b+%yv{OWg-~k3kK{j$p!d#{GavNe`i0pn zcp7`;j674jy2U#R2Xd8N6{-8@de-pu*_rMYBnL_l_}5}19%?d{O_~lK&Q=ze#5i#| ze?EBpEcJO?2JYCCuAc8WaFJ+Jtx|1F{q?w*Nhz_B3G$l|0~+@GkBZlNifu}X3i^I1 z;4c`T2nqCAai_x92cD7tXrRAXcFm|0b%T&gdwv3o*^n7&2@KZQ&CG$3G~x_sk%m3} zei#VKRWJXzpewM9Ur(X=VutEx=~kbFz27=v9ONqvH9@E#CFH{^yB%xg@mUoXPa;|f z#8FD#J#|1jPT%wogdgK{zU$o9tY&#?HwBbb(f6R(QNS&X71uInU z0iN|ywbBZv9CInr@A^2A6iY67-*2uWjf?Df_unkQLb{(B;d4W3WXh z*Qtg~Yoti$lY@t&M86zjp$aK^Vf$}7MOovu3U7D`yKgotIp~oQ6Pcdj{MI>>XT&lc z^N&XuWJao`NEX$0_d*v>V{VmiGVd)A#8sFAoQL>v$JPM%i8ca!dyrpiZa%YsBPR1v z@A*t^x(Mf#> zpC!fW%wm#x}s!jmSsLnuP@NXQZ6)SQp#kNK#MTvrqI>ihUmMiqQ; z%rb{w(%bJ_*p2BsI+DHSY*UtO?}Ml=!%3b5>}0UJciMqwG(o+e+_Ta(xXae&Gi~%$ zJ>Js8OpL77V0;n6UX$qB0^9`{YS}GhwjWg1ZH=69>0oZ9sJaIlE}EOy(;}h6qRvSQ zOMdl_PE-&`pyMu^DY^Uo?lSv%y~R0;*HLkCcr&3)4aLq!Lrw1MzYbE+OXvxZQv)HQ zAi_vilVHRQNBj*wH#}6?+#N!KUA0EplQ(5%jXb5a;|PlMJ5E+>&~-Yn18$ z1*7adWO_@a@qVXY4EcJ=wM&w~vAkvcGCsr-S;|UuE{3a*QxXCdP_e45V7y*&*oN=n zhSONY_;3mBQNs(A)!%PCMFlrgpkX~WpEQ zUoaKGjalj656_@K9oxMwy7(GarCqxjaQk>ya!CKr_4aZkVo?3LekIA)M#@fF?oHIp z37J2`Z3caM!2VfInmM{TTc4JBj|!i~>im_NQyvo9=Hflqj2QG@<+zGN-ZOjc^{B^j zXb`cgghl;93yXmfg*dWiSv6MmANSO!5Q)n2($MysqBy~?Ys2eFhh~@QZCmlSc#I#1 z33{Rcy0*!)Fz!c+Nv!4jJ{J5vwwcr>?T_b0vG9_uDKN1KpVl^v?);SuIr0cUFRo<< z$zRKh5;1-eW@sSFynut%Bh0k;Ea;lg-a-LR{y0~-(K3MPkh|X0!+Mh0$NmhOl7om?3Dwmb+A(X&tO=`kWWXQ#20EiW}+v~PARY} zh(z5;{gw#2iI7sMFFjC_S8X*wpE>Npzz*9Z`AmDvgs;#LQmMnz>%W?N$}W`pn0Pka zHY~Y(=JuBIGO7N=UB;f&gGDPC*SU9u+;91_HhMX;-A4Dieeo^eoucmZ59nul#F7P6 zl4LH0MJfSCJJVhH@l(P&knW2jzPvoK(jL!hYo!oMPm{}YD6?d<$pwd(}H zkZTFUOWM?l35`mRJOO3$j4vn6ABHj;u>lVFDMjVq$ ze5i14rNBLWGiOb`v`NJPZzW2+F2%5nwr+)XA8VI%HibYSrN7u*l7d`3+v@~U*dvmP zu&ZT~f&M4u!OzR{t}A`ynUq#+S*98@jpp^ob1SZaw~DSK0F^US&@AaqI!p6e=siLIY@M|EG`~kRB?n86wITI*h zp^g*w8@|~E)+5E+uQC+wfyasvOqTMgV(;?PB3IiY5)gTY2Og7dEe}B2{J||wx#lZ% zYFQ9W)N%(mNG)0(%U7;S^;2)yQDK_%-s1?b?qD{`ab(cC)w$5BW%AWtt#-O4CgX@4 zz#;$XyX)wwvQ5-!4C7~&MVDzXfinhmG4+ADVf~%cIv>@1h=wO4qLi6}-$rXahu9z6 zluSB@zGU4;HLTFIJxyqa=!i&-?0EgEH$=~I(E@8ctytg^?E>|idgTeQ1uq4zwBo;V zX>F=%vs)FJ&zwBXwR<%z?WYc{f}XS(@UhDy*Xgc+{i6_kufUCuM@i}g9GAMWO@geZ zgT3p2OO39b-z38kWC`!?lLa03d9k*2CpgSXfq?3#A*82csZWqXwyqJn$p+qr(eYZidTby)H4J5lFd@dLF~-U zS55R-`z%OaSb;rzjxk`hqpCLaETY9RQC3ul)bM8QYNsfc=^^Kj;vpNE7FzSINDG_*xxlm=hEsHMjLF~FDYt*Fr0*TyW9LwwP1Rv2 z#sPg$9y024_&9b)Zr`)p`n1-A2A5FEc^+nk-8(-LRyvYmPMSQwMwv=H|E1cbw^lM_ zQ_phnTF#%K zFaPVom+$`b@{!zrNLgy^tr?Q#0mI_iSsy}X9c6{eWOQ>Szb;JF7s+Wc$pgc&eJ}^n zHzI4K{k+1xN@N*E3X1TLOldPQ#0f<=&etfcol>E+6e+LV!h!Db3Z6{S?fDACqHlZK zOV()$d597(`zT!sDln4x^F{lsoGLgK6T)w@T7zIR*R%YUu)YqGckQDrJ=xd>mIPPa z-C1Icb0P6X_&W|4K2DZ5829NZbL^+vg@p&`m2?P}Rpk3ve^RBWFNe`ZcLoT?%X-fq(+Ki3psG`;2A=e~UP5o^aXsH^jhd z)TIu_Z&mHi?#GKcyNVohvg7J*k!QAfM!vP7qlsI|{_sd1gfaoC+LczXdUMNJsTe0b z%FN>roP$lh>qDd4Z9bKj$**tyro-xk6vs%ro%;B23LVD*N_^$(OngQMlYF1>4)VX~ddsLP8?F0$16!oK zrMtTuq`O19ySuxTlFp4%A}K8m(nup6o9^!B-FTk!JoovJ_k5HO4#)6fU31O(o9h}P z#|!TX`a$cOisx@QUycCJ=4JC|IXR|DdxU}qUxRQSQl*)q%~(QXSf*?@_zw2D330!i zIBFd&j=6_g#2~MY{MqmxQLa^GpgEn<=pG$f2~DDfwrjNGUd!7axeUvn-Wud>>}5X%e5B^NI2a`}hU=oGYdp|cU^vkg zD>!aV!UHSC!mCCu2wG~Cd7;|oY)FWU!!AR`)<;^LWcN4FqTctNPJdm`Py8Ag*#m!!^~qdD*7QVQN}>EpG0x*L7~%{-NUK%$BDWa@8Yj@*h&(LeeL}_>###icUIGJ0won=x1o|uT=MoQsiF< zfr-ErR}fiDp=*WT_u8BeV3Dj|HsL)hT-E$}`7Ci|;`(W{ZPrq)=GM z2K;T1Zt9GB;pAKVg|2W>jHWiY@K3QH19Z?yHaOJy{QS^G%PxbBmj96MRQNfPF&kNC zT!%c(Dd(}lLS2JQ7W3%|(?)AMZ#C^|+O$J%k}n^S)h}|1y7P1rQ_J6t^(LNX?=K$t z+POMHZk?M}t<4D6Y%wwl2B`uU11%Fv3;sEiRwp7}hgWl-`jr%9Jb7Qf>ZXL1YE2>a z|00}3ErBzwx4qPLq8eH)AI(dQYR(_y^F_6~A%n|hBiQ>nR_>~~ykYW6JzVGWslZf; z&dG=!O9l7aRb|yaug4GHCUsUw|McL3-6XKR^HQ85WVB*(b!;@CRr0Vc%TQIA`A2!}IUYo49Kin}YlU}dv0peDGb?LUEI4le zm?7EALDz&uGU=&qOLKOKdQF#IpuKa}$dCyjJ&-B+!xTP_kNptkV^8hS9A^26;p`Fv zE&l;aT|5DX(4s9!Y@H{JsDQIZ8U0u1_WivNf!#z)#vK!BQs;z%Sn}epzysrX{}9mH z#d6=3&9ot)V_d#_S}@?oU7oR7r-&uy;A^`NPX)9YFxM@GM8hbZ8j1BR-?fO|&d`X83`;)mdki#Oq#ElP!LY7(E z;E$q{=dJTl(0A>A3idn7Vm7XvEOaGn&3@`B1GLZO7DiLvT{0FL*99*aa`j}!z5{jLn@m2u#lrXg>jv!!-OM6Sr3J_wPrheP1^Vx#jqF!9Ar}*H>)&{j zKU?m9`?aaraoma+n<&EzcQrXuDNsqK^X@n^y2Dzd)QMOE+$;6yzXBA+c>5$g9@&S9 zJ=A(*?Om^pDHFYDVzTx#k0zG$%!TTq~bW0Z&Sq1VU$8~ikU zmnWKR#bo;}=z;^Vx5F`8J6$dy9rMwfll##&coYsZ0NLBIXpFS$w$Nn$WHhwmgx@OU zRLh3qV=vS#5N!po(oexG(qcKY*Rb}O(W38elG->+k9>uO`pAj8xT)Dt$=LBmtS+w zBlpp49eIT&?b>?KXwThXK}MHJ=uqaXdZMm3(o zstbx0vM+$tL)W77?DB@x;|pZot)sZ z3TSJ3K#X&VYIV>Dpf6;;Y>E9LMtD0~q=`-irylKx5eq}FD+xoFb+y2gxp%8!q>Wb` z;QqNiDj+^Zswxgk@l3P?Rocl@)rUHjzm&G-a4FQdU>L0b2W5%ZRX3myDs|Vmjd{(V7Ss z2eEfk$gPCoDjXWJZu}u!4L+6Tb>44Lh?c{Wvl6a7Q%#TkRJ} z24HL(j<)Ohhn{%gp0XPgGJ}D z!rC(101*xZ!qG~{Nxzqj9|2}~jP$_kwa-m;JD``L4Xe46lznU4xaO|8+s^igQ}b^TTStn(Jk+<;D;lxFQTvKY!6ji@l%;O5}CvoJ;W;8Zudb0}{5C75h7k z;1-JY{Pn%Qec^3VxS~N&UbEWSLa9s zQh9@DQItfpv(wF&RC~;?xvVdw8STn|uDbs54@QW|0#p$$H$a;W2ol5Lg3)ZpPoEsy zEX>*J1=?hry6V5N#f7Fsh|K1S+quc!ESlAVtX$^n--U8$)0GsCH6=#Iyq0G}c_jlG z17R_#6%cl_D9KR6Dp|6%G7`UAXIci`&~?{=HG3ShT6y8PRQQ~df%a!0Zf&(jhbtO? z;ey)MohYjWhzUB?+9vZ7Fl&IcF0aPz%J3`lAZ14ey1^RE{sriMmRVLV-}3EoI6!H= zrJg5j_hR`cHci>Lqbm?TGWmBhwvP0e&Bp*-vZXZ0dP{EB!4!0nvsE;UF;Br^GX{H+ z>53W`I#Di5niXIB)-JK>8IPLZaO2CoY#+8UxW+;Le0Zq{^6>#L$K*M%*eHmPzq?LM z$4bL~{^3S*a_}tUlpeX2^|__H4RgFyaz$}nRId7w2XkC1iuV=?Js#$Cad6PsK5Ak@ z#A!^mL(7j_j`9SY3->6rT;5J@lPve+$LBg@mH^1w)M({&f0J{81IRnjH(Qa=AQ-C^ z<`Ud-S5;L8(~J==Xo>jG3SsrATOG#M<369@M;#7e8c0}6{utrF)Oh#b<_;3htMknW z8p`^IRq!8Uig*I<(SE*W8QS)xxe*iS|MDfZ%n0<&Ek;d$GA+~4u%Y~9r#iK6Z!|Ri zuc)3iESDjLDo{=|uBIKQ8uB;d#H-!oJHeh*8)`g2ngZp`&bkYL%M4J)1JB=4bkLhy z>CaST&ojQ8rRg%0;QdfF#{j!_@WiuxdHZ>7@`{X)nXD&1<>rWHBjEnoBv{W+JoNLm z%u}E=LuYF%W$l2a9g(P;xZ1pBP|o%kSfdU+MQacqBC89ND~^p}x>x(76lMBk(LF-M zF3N60bSY%yo8_{P2!|5t2?uF?0;qF7SfUMZPe=4(o9knI&}93X&jWhs?~RuoHI&Z{1{;|8@8CeDwA7)WFf0q5sl=vY@5~q9 zU969n4JW4R&-sBKrf$K!)qx$%YD9z;wcb_o6-&n}_Hh0SR|V)Qu?-@p{L#{&Zz-S|S?wPx3qvTUZy&0HTz(&wje%|6NiFp< zXfFo*Dl7e`*^U0{GCpxbRVie#t@ThbXa!LeWGZ=(IdI2)eg}1k9}reumHNZkTuC#@ z+JgD4QAbG05v$%RFv>)eN_S8cL0Ya%8z93E8YMw+jA-{zcIF~(DjP2c5_~Wv?05b3w*F{id(#D4IhX(+K@UaIfyhG z${A|!X|J*?TWBzU`^#ZOK2eQFmXi+^Av-;Zn;s#c$yF;iLarY+Nk;cV5SWGcSGNkv z!z*Q|aT*^f73EIlFpt;4TD#Z>c;xGX8FJaTu{*W^3ZTC<&yn?5N^Q?ziFuAWXeB>A zatFJM;|Lb)xfl&Tr^?Js)YcOVezKfvIP`K$uVCBay3f5Kl#?b|9yR-OL*BdpgK5Km zxh@SGKW|*&GPZJ0Ei~0FDN=;x0Hk$Tq$#f+@L05xMaUqorM$5x=USWC?yFT_Q2fb}_8~oAz%a$z$9G zD8Nt`z5tHXy^rZg^vc8CDN*b8O^sA#RRknCn#5l?&?;yg}YA6^i7?T#qE z+ghfxx@p|!u+p??T+W)T+?!eHmaqy3(-5!>$b~8$FJIcaf-b^N3k9JIMhcIz4{~UW zW(XIpph;4xzR$lmnOsmQ13KvEQ3k_dK5#wme;f<+R71CK#m=>%?MM&tLItL7PMkDqi&oss!2djajc4 zybp9vPcE-<9fL9ZWwMRzjB$>k==$L%ZgHqkV3A#sOR2{30b*-Z$>yt#7>z-Txt5VH zp<~$MS<*B$TyNs6{O9Gb8soXZS8xJ3v2a}VxAopp^-kRND5qz-y`Q2r0-7{oM|O+=d5Bm#rM%G=K$8XX$= zB){)ap9b*Se@{^m28V|uX+61Ggq8U`7pWBxYxB%jmPZuSw(xBHAsGny0*B4kM-Mea zKRQ<%aew9=P18MJrP)fZEP>IKLg;+xm2v1{l4=K*sytNR6?J*I!lz_@JET)o6yt4! zf}T;%&HcAraW|u7;;FYYr#aa!t{(qp0p7vkS*=QumqTx0lA(;_#AXyHXUzTFMeT$$_QGo{0vO7IvWcwPi`N!*v8DUfJ5ORU<*=wZPZI()Y=y06xd105 zu!(7rJKQuhMwkp+D`g3IEjQUJ3Ni(=b*r_muwQwWe`)qzR7m8P76>2C z<8F(&Qsdpb&vH5Id+_1=Ch>%1aX0esEH#DS)^lFD@?yNK(}%wysXkjlW>opHUwq;h zlq7R2f;WB%8d5x))nMo4fSB8lKaX0Frf@=4z9yYlm|3BH^lU%KdGkxptl*vMdUc?T z{F8gJs+ThLc7lw7ENuZKt$nbW)svPf``{;Y4IEstbSn?{mnZg{?5mpfde6ESYHex1 zaRZr_p6I4$ob&}ZA#w#$MXu6@BFUa-hPoOiFGi1xnvpgv^ro|3%R9k3qoh<_BNmZ1 z-}0*Tivz~)bQOS13c<%7onE;l{Z_2``fUo0<#b#dge!gK);wJrDUoq;Hz(<-z^tX_ zH9ium)%R*sBMPBs3O+J?Ij7<=`+}`s-tyut&0Dds7V&Elxl}&}J-{9tp%G*b=z|lwjyZBm$ z=!1z5;$<0UMdnXqeuk@l$C>~ZtRULX7iahQHW(f3DVUuypx z+~3>rL`SO_KLjQYbpNAj0nuJhI^QGz_mTO3eDm8zve$9OZJlcJ-zJ{mIoi+1x$Znp zzOOOj#>!u-SPXrh?P@M*hsR0Asfa8P`mB(D2kPOxYxAJWkVp=0r5&h3LuaHsoL-i1 z!Gh2KQjQ6E(>L12-V(mGj&3%T-YY;JVHE!$|Ks$a(%d&l`St}0-9$ayp!)u=*f!5e zYRLDTu)hmE6Fzh5uzUY*EG+DV&j`b#H^m55EvEl!etPlze!r!DELdjSTs@4tgZ4Wk zVN$fUha_PJwo+6EQo1ac%;D{r&ow_;CzN}WAsedg%yJd9_Z1G|GkeJ$R`RGgXeQZ} zew3J1`)XSNq73Z_`m>6)=~1e2HVj$?g%gwYi*+x#W77VVBW=vxgR_?xJO$VdAB}-x z-3zylyvJ<2v2IFR69DssUnk=R^UzIG63OLga`BFTHpNwwhysf82a1ue-`<2@kpg?n z57MS_(L*F<10!7of^+fxnK6S55qCeT>``{}UImZMZAGT%g=E`g3yzB`!o(oAQ zIk)oI{9aWky=l8qI7;1+{}-On-{??p<6D2iPP@e5931ewf$3p#n2P#GOz2KLAfT7E zV%Ca#a;oztM;Y4NYIdwA3TvJtnh9LdhA)w)dxf>kZhgT)@`O*dl#Xq?SNv7W6ze~B#}3FmLB*JA4ZIal=#Ehx5m-!=8MU2eYjOn@k)(VoArvw z#hhH@nUM4-(bgjyBhq7X2`4D3j%dt}uRD!RdB_rZp%r}!tNzcgA_xjG*d#wRezFK9 zMPjxXG-N9%S%QPDF)t!_9tKuOiR`x=1wGA{Uqig1Q(G3Aajy+KkgzG^@Ht`6g$$@* z2l^+ZI>4{ky(KyDqkSikc$G)qh^`Z3hSY9(RN|4TO-<;lbq9n5p9xiV2FY@GTRIG7sMW7HlhN(G|KxW$Fqac zcU{q#p66YWx@{5!cUKEt#z|NF3f@|kI^dPcY8!g{>((X$XxZY=k8P5miG*~;2N>NW zpIP? zLCAO;CDfbE724t z8O~-6w?!w{>2_1X<@$N~1p9RbpMm`kRe`^K3oAP3Sv8IE`BbEh*IWN>z|Xf)*uK5| zkl4$uf(kQ{h|ndCJ61 z8T8d`vt1f}zME*7y$OCEdGNTIG7c072Jb^%+uL}z4Ue%p*N|58p5&@mAU*j%&VMcDTd3;f|jaFe8lRx(Otmyh~>1;%F@^YI9X zub&ycYSZb9K{{YvF>%tmqr!i`>q%^EwzcBeTM=5i6+;36iu^Fb@t*TuLq>QXA??o+ z(1E`@(t;;aLI4U;SgPVqeDU}aV#^e{E@r+y^EC~0JPXQi2^^0o^k#08p&k(xs(@#F zXD!V?Cbd5_WrYMK&Va0j@gF=y0*elAD<2wkp17VaEPQ)EO(*TPAIeCjat_LDKj zo6-VUww>N4%jG8kM;`S)BbMnuu!yYR`;BC9?4T~+&Wr*lT6%ZDhCv%9UPc~w(^C&Jjf{pqf%(E< zCms!0eR>qJ2B7sYn}ktq|=_AFi>Xh)v+OHA`y0YDv%*GR>d02&*pk_Pf(z3ts$!7eqo3%2675{Mc)7iDkoy2U*QP!3g{-SVGDQm{r z?i>2VTgvb6pu`b`X_uzn;&DI>fc&}0+va1_;0XH<(5DA85`HW3W@aw~rUFHb&)-Np zKWD*`g~(xy>h}z!lrFX=NUzT3$OPJvpUuKqY5CTJ{lM%UL!L_5DVWY-&daVChWa` zB-J2uB>t`SeBIU(dY)6MUlq19@~-RU-g5 zk58FcQERGP?WG+5q0@-x;Yt#wq_1!3PW~bK|3ksCssLYQ-WHLMsa-z72I42}ieq4< z&>zJ{@(8xTVz9?emn1ZnFZ5Cag4_UtcR*k?BH=*Y#z43Z`-8w(x5V+2EfS4OYjgch9YYl;iH{;@HZLh}9Yz zHXk4k$7I+00MCxHtdr?$)P50yvV?aOeS0HmEcg`_DpyqGv)2{g7;jYLWJI+}`Z!8R zmwNVeLeykFvq1md-ZZ-3#5z%IXTqL2kZK$m}PF(C~ZvCK}@Ng&Hgsqx_u8^AejTX#O(YB@rd>e!+0z%WI zmOzEU<=8hLvnlvHwk8q7nyLyCM6=7kRuzEmiiHhB*_`x8xzedL)LQb1_BGp|T3(Yx z%`BmZF2;-R3~XUzz{t@6T%FlJx7Jl!vr zv{aFAJM|t+vO%4tOF+H*SH3Oc`Ri`CsZ5ktZ6@yvXWf@{{YA3afgAWY4+a;R$%Kx> z-o_suVi$3$KD0@T+-Q0HlVp$YEczY)NQ&td%?9*;g!rl)-+x5-=~?HS@(}ttH<&C0 z&`2L#@l#g1kQSnmiOSx+XXwl!a(FnI_u~yf1bcvi>ng~#(Md#Lm#AZ>W+$SM!&dO8 zscVuDN)p*qED{k2QaABTW)%}7EnY6GakB!6V3_OIAzxD*knaJ{GEA#r^D4_y-6Ldw zlU4IMl+BLZc?=F#`qh>nzC2!hK&;#YR`4fZ?QXUSVysHo^F*%lWDbdsc`(W_p1X=; zhM$5SW-4FG7N((S9VwaH3wMt&jvkYbC}FHNhvD-kDnAQn=>Scmo@z1pq%r4kkQeNQw4qtXG?i{H(ip4Gr=($Y5@>3^ zsbwBP3L*mHG2iELzn9}4lUCR-y%S4Ao;VW_q-O8~qq!^cLI_l7DgZRNdNE{qMWy?Y zTM54MLi!~s(e?0Ckw+%Kis__`18FMINaT7$B8*)dgfPD4+bgIdQzcY5w*;m!h?z2t z!uvT9jg_ePbemDR;dlH-Xve!_TrYV;Qez17K{X%YL@oG6SwHvq1aImIS&44Wl1Nt5 z2%^@#H~&%zOV#S2fAB4@w8E>$mGI1_C8Y7+Xa#EE45JZ$uJ4{MB!9%=8i```Uvv=Q z)O+@T_hCZvJk@)^(d4)B9reQVj#z4?C9B+O)(SQ@kb%yk*Tyf-)xw@L)c)3X?DtBk zg!Z@@J4}A zq8Fb%2+*GX1^z3Rq^w^Iiwhui7y^1do4B(7@WLZ{a?>*MJK$+uxQd}iMvrF}55-yh zms@qU$%Z66drG5ZO+b-9TFrykVav8{g?xG4wot0VMvkz}Oh~)@DddDtI~>ZVGcRn= z*u8Ie_vCyvOYNIX7H-y!4GClnN{(O(T!if2y7nSqhhC(me2mVx^-y;j$-T<=c^Ni4 z5$pGnlSRZ<2Th)#-_qC|HTnEenG!CPjWsUkW_%bB6!ru?&}~)FaY$GZU>~&k9G(9w z#>A+ZwO_Z>jgD{xk#lxH->|4 z-{f6usqjZ2FhZar@kMifRZ9t@5Xaun!9L`_`q}4~T&nH4Cechr=Pwe)McX ztj{l&aW-P0+H&`o$wEOzEvWu&oictgQ|WTVr<3L} z6H}k#ym2-u|7b{0Dreez)`!FFC}o|QL`B1b|Ey?_cMO$NpXLAy=H(CcVbS6>OD!Z! zdgaGT3Uh_W1|GbkybUT5BiHV@&X|YYAJb}Uz{*On)A?3~5P=S7`GM%>ObcMLURrLu zt60^LBjyq+D~w0#lg3C^O;9|oN-!2`F?+u`%?y;m@bwvA=qINg^yx~5zm0cXkewVp z%DNG{p%2D|zKv5VLeQkY>9z7ZkE`K;*-24sNwzj=fmO$zbFt*&k|FP=&9u=tw%@O) z7Tk#!vP<&+1UYvLQ?|<^v7Q0(cakpBzFQ@)m}!(NUtaT-g*r@K81k}nUeYYMjRjeB zDgRGB4^{cKy|>24JjC#iN9gsH_R5D&3;u^Z`VU|J3*u`*LJ5GkAF`zmnOZB3q$;Rg zms$a}9onjz!y3h&F`z7N+OJ@;*lCZ5*4sLb>_x~Pl@ybo+Z7I#qbPXQoE zk(!qg0hY?0u5Xz4?PS>FWpXew-ETGrHd~3V@m}ORc!f2Dz;2Y$5}~q`t^-!c@(xns#$16Tgi^(o6JBM!80g5n-qjEDA4?I)*6|TGrmtt+ExuE{BLd7Y!j;>YA^_Leu zI9BfNhM??zYJt(%;=WlZPMYaJH#B1~ego5i@$tHoGl~R&%vxS_8@aFV?e$364+d`k z#%q4G+~tjNx$Zg(luhP#8iW#=&-51+G&XN3zi{cxM_Xbzv%^pWbPF73^^)B_mAFHI ze=BPON*ghWXv@GC9y3kQ#g%Q zpUv5JzAgfo-6A7;ffMo0C;c#`#Nw{91+OPP+Lxt{>^Gz)w|fKIe&9{25uB(aEWZME z9?uTF5M}S@CgRDz>0&7o+)$$3J6JUhio=L`$_4nZupYh09#%p|83tpaO9C@nV72%3 zKCY72iwc)L?a!50C|>0GJO4i`FK^ekXL$sB7c} zrif>>Uh^OEdU(M<4q4aKCnnu2*CBiT<-^gOpA)$32sM$GtiOoY=XcdC3hb1!z{R^; zlli&?hRGB6Uv<_=rWGdhNvoyKHh*$4A#%GUKPmY~h9_l!Fn4sgjgVCq?z`dunYH+S zSl=IQnj~!7*owc>ssBiCq$)PK9rzGXseJ7FHh1T2WoQ@_&}0)EjZx0Ub@^zxTnfs3 zyC~(QR$$Iv!LA^m1ck+1tyYo~Cup}nKa{>yP4ite3iyGu+M+r6OxS1?ag*>_EY8Mc z=Bk;Y3H!98XATCK`QXKQHRG+7L?vy;EPMn%PQSg{z+_uS6D3Op9cxAPa*bl^zs>9> zhxL1_Txh|Lqiz02`KUmUUou%Psm2UysHb;%^M_kP>Em4Tmlx0%q8>O(n@zXQ+wUgx zQI2CBo4)-hwaTxRwdRO}tK6)mv`rfpz=l<%1<9YPHg_SzmYGwlBWH`GVYYBpj8nEa ze8?`Vm901{loS(`W6P816pexdx3gZPEY=l z>Ow#1%l`2!y!K;C=*p>By#i5t zmu31H?H!P5;^++Ia5U|k$cpq}zYbH0-y7B(yW$=#uy>EgGMq1A$Cj^>Ck%i^S35g-&SZa!WC zB0E(^s`b6SzMmbtV!qO{NmRGs3#cmIX-x=8=WiKj?eJr&@pq}yEmU2Y(9u9{r}NVUD|l;-+(Er{}($7cmIs(=!? zW#GfFFR-2BOSg#XHm&;KAB*$uI1FMWOovZoEpOPOq^Yo{Fk z+ID3%3lI#vr4ub8F%BsZF+?J!Ofb5h^U%#rVgR>=`r0%J0;LG2Wu)z;rJ~IF%iO4o zRiHxuWI+*pworFEns)l)-0g8gWvV&```2Jid=j9d^h9&~*cUSkX+_E2-~a6%dwlQe zNxw_~q-~g}6$2ln488R>vxNKruk8MRA3ASNQs=!I3T5MUCxy1Uvo;8=)sdgqenn|< zc8P9gg3g`;`Tv4jAnOE%Z&+#^l3Z`di+Fr^XImg9nj~Whi;$$yEkV))M(PNFlitS` zP=!D2PqjI?_1K{?c88N(YZva!Zw;T`q;B6agmo@~=8vWniCMmVa5VU71sk}WWj(g& zVt+{D1At3DlYn=locP4+n=Q=Nr2Uyva3tHV#VxzX(zU@m)nwt*`E7cgERDjapob!2 z;~aZGGxV0phGa&XI|S<1QvK@p^~0^bRRU4L=+wjn;jT^!2nx|(cn&D~9?8f2n~m7u z+4*MgM>bD4y(cLBUulcb8QF$@1r`Rq8C2JeJ-)%rPZ_s0CJRo(b_~m-H%qAi7Wt=Z zJ_y>C)$ggGlPckoa3WX+BXc}m?eTYuiL-0LNZO(#=$C!7i61bLR?8^+6ANS$ddC#0 z`6Lc{zG-NBDb6MqVb@5iZ98oZ&b(&O(<)C~0uymp8NIj@OiDmK!k-_L<_Gn{uej#z zK$Z$Vpf7x|p6D$C9@$=%3hS2jvpo5||%N6l5p^7trEte{YWf1ZOj!WC$cy;W=;PiB+gNGWeyU0Mk zke!H_ZaIoi*4OrYCjf1_7~*VfPOl|@HZwyk1p9o*RdNyvRU{+ zCAi3-0)QK&#Zo5YH5rZ$z{M+WtIH7Dd61APvk(+j@RPN?BKP#uIgSrmJRaw`lP*3=K;KDzBNxx6p~()*9VZ(t2?!cyT4T)i0|^!4g|LWBRsZ3}eJC zG{2F3u_Rz|!n1G3o$W|srkN>SJ*!Q0zslLpOr8r0h?0s`$TsP+AfKf2pfLr!tOTzy ziKQ?gO~qqHxuuPG>=Y4DAS7Z>E6^Wh(+n_0Y}iO#hSUkf`pYreymLvkW0Iu@&})85 zl+y$8K>jlGad+~MM!QhkIPk}g&Go446W{5;h?pTQp+E5L@ck79wGlgBOx4PPc~WvB z!&N-xrD(bwxL^ke7Jcv`(#|Kxig;(wjBK;aj{x9!03>l%6PB)$ZYX zU@XkfN!FvM&8!nX@ycA}{4qRic`W&R`YFNm$>W<4Zcbsd(HxOWGs+51$<8PU+c5TZ zJp9BX1r+$bk-Fb^5F?$?qlf>Fcws*<*60V=jj) zHidM9UsKlV-Sn6;QT2M zO6p%TP=|y@9qJ?m_(eiDkJ>BouXxgP@xu(d40PAl(%DI{rIlhU*YZ>LE53;~srj>m zA|G`h`{1&*U9M%Wc=*m$RorLJ$kNu)nI%?s6^3k%>S@E{z>@p76ce@BN)*6z;$kVI zj_C=^)~eiPHC-j(^k$=cz^Yll><60xPOhmHuhQwapXYGXT0Fb^ za(Nf=z2T+-dhV4cu3b2&fZW_b;quCA;to76_#Hb&koMFd*r4v$<9_jKGYxEJJ*_Y( z<sd;7u)dJx)1SSguIC&-gZ0xiV8#QD`{jw(`641#-9( za|QBcUg@!bGmc3ec*E_^3%^JHMb;yp%LBW{U(C!f7gVJ8bvYZq!FukCIQ45wR26WH z;fj`UU;RmBXTGT0l!I?lPIz0y!yXkaU`37C6b`N)3rJG4#^$%kgK==5gwNFB$u~w_ zbH*7t$hegRclzKJ3R2Ahb^+{gO#N(Ow6et|I0bo5xq)yEidCMBVfp4X_^ARwwr_)Ch_P`(ONL4T%5@$8RpmZ7&!66-#?lf9+PuF zr3EoT6@={vtt%3b#P+_^7F?c&pikG^u%)Oqh~9Q-EqTIrFfe&bv}xAKR_{}#yluo z)RZ!P@&&r+K}V7Z43I7rCbaKF+g8wiXAMahDrlf%kd7*;6!bwEgrK^QrG3#~ z|F(W)QXKO=Fe+g~Vv1A7Arwhu~xCO8NR5Wr9!utK(x-1;8y!6IYEbC9adxc?(%l0-hE z*phMduiqTM>fNoqTj6wtkZf<3G^>BPMwRR<)6x0d=XxbvzuSH4gtQr$k!>1_L3gJ@ z;ts@IrPD1BnKEoXVgqf;W~)&a5V#8+UXk;l-idwcOcmfVm=mubIH)SmLfL0BwR8~= zVY)SJ@$iBY$DNn*?sAEpF_buTcqmXz6PT%zpx-^Em^c@HumqoYwJ~3+F z?IR&27$s%fh zk=-|dKSy^IX_wMqUDbM#sF@q(T^H@~k(iQdFc7AUjB9zLtmUGfu1U&Rer4*%zvgPs z5_K~JBPKVRX3wH#!7UcBFHAxY6IDCR@8Hr;9iFtCWL(Ec#uht7+9rN(c4rp%S&us5 zt6>SOvhR(tX|>Mp>^bh(IAm*+hCg}#C|RHu;AW@*u~Y!8gY((2t6FWGf{g?1%x48& zN6A3B`##0DTYOUpHy=H2lIjqC9(HtcW?doK@#mO6>^SJYPmrrbWYcK0pWm@y&K2@5 zr9uxfs?sn5&ChWn|MHnWr#j2Y!5z?@cWKrYqA7C#yRpE{^3VTf4`PzKIhMvZ0^zJRYk2(XhlaCADV+cVP1!Q4lS~X*Y@Fj(8xxC z?Z0}2#DTAZDkxCl^;DGz?9gji`0oitT=`dQd6=gd{Wr07i%Ryxa(>cJ)LZNR8p+>! zaA;cZd%lBYIi;aSrA8I~`u)EHH?m~m_cCHW*kYI@#i|)^Fo}#RbxhLkTO|kB-oMYf zy}O+26RL>utS=dgncGJoHs@!wOMBV7-)tF8Ff&D?oGY@|RhQ%Q5kotVKWGn*kvuZf zvX0z4inQn*!k9-=#$3J59$XtdBd6>>wm-QGI5>)dczp}`I8&;muEr=4e6QMP zt-A4L(tGAv>0xQsc5E9X#@8-iF0i(J9xMPELQ53>+vrPmRCq3yyUe#ekVrymJH8*e zd0Wl2&=}3p>e!D9{x}}GFB64_@qotX32A5Q@A?X=IRl8jV_?_HJ~-I1DUTs{R6{8H z{qZZMu9(aMP#8#>lLxi-6CYbZt#d$^0Ce#lIzBFY$55TX@CXQfHJS_QB;asWDo_Q| z{JwaqV>;P#V)gbUoC`XEtTZ|k^^l$z3C(IiC{2kuqH6uxw4vCSlK}#32%})GdJlmI zg1MuI;*owX|^_#VH5vTz<7kH zlPd@Pnf_}dbBjbz{z|gIGk*;<>Y_L6G^mq!rCc^)RB=p+f-3K zg+lX-LWp0=t^`gNs!H;h#%f`J>v@#ON#|dR&sLrS6CBE3Aa+Y+%ouVND(Rr|y)d0* zhZdS~hahEbk6{9atAK)(R!VECmUeuyB{tPB?)~Gzo*c@@-xbC`r@vJmo=8Qi*(SSx zHDpVm%s2LP^V6I4!(v2axETsiqdWyDNF%NLvu0(9BV1`on;)r|T&i@}gb!r3^bBQ- z`*WI6G+~<*1t_a^hft9nsQp6e5;Y(pSP@`xX@pR(RNJVpHS&OX4+&5c|436A;D=Pl1?c^!%I{~blUeVU5KZeX?)H!)ZSr!l!!`0H26sIy z^Eq?0ZMe16aK(2~kaFZY?bds2B4n>78;N_*OIp~Kyq4NH5i8*{4KaWix6^>ZmO`ZB zS^TuC>2luUgZ_i$oXkUN_-44DB*B!(t^Yno5qR`q=rE0w+Jeh*ItExEPI9hXn^A)3 z%y6r$H|@#<9y_TqHO}kGpEpV1_jsy%SKq}3<0LsKUoL`=%J0~#ez{=+IU?Xkp@2N6)&%~L

fDmUIcFw;yj!t!X? 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Ν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

    kZ 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

    SxBUQ=VqPqT{xfm6NrfG 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

      +VJT@Csww2AA~4dZ&fvKf6m7UWbtjWG+=%? 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

      mvi#;*e-)U?RZ;6&-mnb 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

      tvS7 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�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 z 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

      N2lNiYEN9ot4rSXTXViUCtaBR9E22=S1 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

      M*@x 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

      bTb(oIIi zsJH_?U*iQqfm!3;yyrUBg%jyz1jD2rbE6t6G3?88Vw;$ z@|4%?vcVKLDveu~);o-Ny=Cg;6e~Yg@0}2sxbaOx_vF6!{>SCRi<}AHu92lMqK4dD zh3uTzfOThl0nDI;A1^l!EIeAe0<8X`!48@Y|D_<)?nfdbkPQ|tJEMSnMZacy0f6}E z`5o5oG4GbSRC1$hWVyRxkl0xyUny%aEfi>(@2S$kJ~+AZ3orqu}?~Bmg*P))NlQI2-JUy zwLgAYl@^4!F1e~4-W?ken9uu#KbZ`VH6tt1mfN&lFX&AOgM)IGK^>MXoPX z*lBr`dhFhPuaa7Fah{_9pT>M7&7`3n7x<4MNaDKfZ zO8c5D&Ss)}Chf14wN?!Kv-SiSE4XD5D$+7I0f|TGiy8skgC*2C3DDfZDE<}BD~i^z zCGr{Jt_}#Y{%h$2N9>uFDo2K~IDhl-Sf;TO!ga7L9WF#;(>GiCr)f-1yo@ITBF+_b zn{JwlOY+63`XS>)(Dk(kK|fll-fP)@mDyz4gyhCi;{p9^@HL_9M|Pgs(yBkL1jN4e z3fb@I#(FHqZ?LEPjYUALFpu&wp-m1#ihcFxO(Sv-0CZX z=qnabH0&UhSHH&J@%B^fYs_Z*Mjl><94^7aBbL1Mr4yLbzbl$mexRW@y6Q}(;1fa4 zoR^eRWr^weCV%>CzcG^f)^AWvj`LBihSt1wOa8Df--}X~DFN3E-KIT#@*YTmcl1k> zN1useoh7qp4SJ#Q6|eig61t|(b!7Jpn) zjFf4H4Yik8EL{#Lk9nnpgty+AuXM4hU zpLN>8=9zWYNyFG4E)K5l=87heN$>j=2nr`WG35AUnyqwiZ8?DL&o`CvXBU=ortp+( zj(p7u>&U!4y8XS+T}`7$Jo6-;bP%7JaysY|FC5CTK#cOH2N`hL_zU6e+ImXT}Jv`D6Rn|?X;YVt|;1Q9x z%rrqz6;DnMufKL}DKRQo0+*Arep>W|ZZOJBSsmnEEq0WNWry!t6;oKH{ZKidprBhCQQc2CS)g z^PVdJPk*i#+k$bT9Lnh**WC4de*8_NQIC6v&OHW?SoeL5H~H|(23+{&Hrn1aS0wR# zRgJ`71@t-3*D+WbQKTvbiotW&t%H74s>zCsi zi0LhE;aYE$Lhy*2tYZ9y{q2R-(XEevKELCFS0P{lY;ECo1M=Zqr8}W5dbXI?J0$?| zC5Vx?5?|Ae3$dQYF!9`K^21<5A1c4)o6@ zbL&gZ*YIZ_$aE6!v)ck#=ybribUk^8+FOtXkL%#ey{n;duyoH;pzy0mC#Mr01_1u?SE&<4A&xEs7 z@Sxz5MMhS5;!)%@+jHbY)-Z*iTOrpLm#w0{NzD0l$8k5G^dXr-1WuIxrE7ib+x_H;WJNea^V!DLRY#G0i`sT)h zV6=zZbxPBDW^%y_+;ge;IwV`l9tqf3x3o8gThjG=gvikE1=C0*$+UdS%o=LprRKCG z$Wp?|dmOj-QO~agO%g~nxgFZZ5c^5Q7b#pbF-TR?TQ+grm~JIVsz@cp7x%vW`~VX5<5 zw)**Xt)@`$t?nUieE06}hc z|GYPK|7rj+=zo_g6m&%4F;9m>x=D(51}k<$1VpzQbv440o3))IS~44c@m2;jRW8 z*Q9LqDzSLJx*QTNCcItlyOl#{-ul>>2p)5FBROdm^;9RC4Y$jb<~5N1vL6x6I)vgY zHQvC-qzgi>S8hz**?!6?@yP3*Q=>GE>jnDgB&njzo5-NuFvk0`08%J`y8hGm89>uoo*4X;B$BO*YjgRnY1#o%& zEL_3iSSwn#$wBAIhqj2QqXv(S%VOp^>(*femi`Qu5mRa22~>p+nl4jOre?~)rkM=E zE3Jbg#RcH-*v>jYZP}kx{)XDoQEl5bCz6E;p-?{=TVymd5md(9zlqtMB-5p4$U;NEy z-N4RLN`H~#eOZB7(=3syI(Po8|1vdH$-x&RfP!2yZWZYAlz}i8IxT9Djj(L$DU{10 z@$Q7t^ZsUOI&1ElFf-WbB`pleg?CeD7BNFOi>te_ zG&qHS8xc}hbpen6mG2?h&VzpSacP%~vT z!rEb91Tml-W&iVQc3}*}8l-Je+B4M}4*np(#*x)^xr*GP5fO)!Rtu`hEa~^eGxgJ{ zo_ZetY)gsqJ0B>HBG3ot*%hL<&{5a;D#iO!U%!|sVsmtgT2Ql~UryEOvaWiY^}pDA z>#(Z2t#4Qm6hum-1ym4Gq`OfRL>i=9q`Rd-C6rQ7x)dqt?hpi|L7GiS$A(RJy>s8U zn>dGapXa&0KfddIFaO(n#hhdOMvpnyT5=5gS#Dd6KN)Z}Rj+Yhqo^P-jy2XRb=+Q^ zvWZfl@4 z?3GFJn}%Wm4%x1nL^?myUzHv%ycZF1qpn~#Gm)5zHu&+vbg<7;XSLws(ARn{O2eqBw$>*qT`ypdWuZA#{V`Mr9Qtx8|CLBi^Q<7R~J=F)$P4mR(lsy&1Y;`E1TA*&%??0hdJJ#9eN(X7umNHIP!7JTSD4=xc}1&O zm(eMe&`n}LN^)Wa-(|U%GzFUYX^yx~6_aLRZMe)~J7^}}|3gy!FuZwmMd3??d6{#8 zo9JGNaMR$5Pb@Teo*I`+)O`LCJ2&)0t)5u?Bv^vUV0yMM6zm{V=r=X%CXDeQDeL+zs!1Etm4!8wHc)9S5tRD$t*qndm)|w z=lmo(=5dw>k|pyzc00E$2^88?q@lZ^m+!sVya74l~ ztgw}uPLxbsVw?Z4+}F|aFsOQSeO;cS)J*b5LrLzJcHLUr-5;K)0#Tu6lm)7<>}<2K zbzBPd3JdED*oVy)C3**Aem2y%LQ5XAY*HJB@Rd1D46|B0MzXiH(lVQWy^wA(h=*yB z|DZfY2a4`Ip1`nh_rvE`3bbbB3eq}91(t^}3VaoTSjY(Foeu6<5hLsoaS2=&KK>)Y*QD3HD=&@FSQ&;a;EvaS??u*zhHI^wWZjsN zf2CQ@Z+tz?el&miBC@Ri`?c>07_s?j_AvY&l74p1y_C|zg|ZGG37c;0W&RHN2Wkt- zBThQ0b)=ZfqW+tr`&XJg!_*`l`L+G|5@)Mhg9IEU-w3@QE8+F#WhKtr* zG8bdDU=SU%mJ}E^6;!%}*b6YvUDgx4m~ET7c#Zb?;jU6#w-IjtEqWd|Ekn^)Uq~GN zuvOS@_HJy}GMFa|wy<--d!#E>isr*ySyVe(ptCa^Q=#OySASp?fphsQlBGW!>C@^wE`g$*wsm1hm|mnxS2WuV!X5XvF4xrU`tF2Yb!|5&9)A^A&Y{Sl zdBr8IgSlQG)l}hn+5xX9i2}zw-g|SbwOSI3zV`7?-p;-Jk;B)Ywc@@G8m zdX}4-^NYTkLysLC=R+>;Tk^M>VXd#NXx(_*6O-3>nOy}o^=a+m z*dsUgP2HgA&9#c43lR3TKHe~+cXOWku8ulxnrZz7x7XUM6a=mjI`gW~e0cjvGut!y z9ll})CyY3W-*kC8dXwzq6UQc7f&P`&(I?8i19z36zQf@!eXf?RKZ1&XQ`nKS}@%gAZ3I4!^D_Z^9) zlg*Vh3i8nmNf%XZ!{`XNyM@zN4d;(~2XS<$ zXj!IBW=S3Y-Y`FhERu3=RVwgjK!P&jY!wP>m>c(f8arb#kAmp1;gE8PcvTJf0&=O) z!KT-mZVUT84O;UuSF_)R zeCG`9|52Y}tvpdqnEI&b2Y%i_EBAEyL;APno5HV>qI=uKt`t*QAD~XJXR7rSE@9M# z$p$4JZncf<%`Wn&+I%#cDmAVBNIS&OqN_Of#F@ahgmrkjM1)|c@?dDSE+!2Rb}fyU zCFmV~ZiuB!#*F3_H&AwOygS#l2oyJzT~=eiujN&WUYhfJpTStFjyd7;sriEvulgy7 zmKRf_$3sEq+sza-`wNm7ScNMRCW)4Ruk<%vi!D|xbGFqI-D?zXD$@^*4VC|Sh^M3A zv!z-`#lPss-)S*b0mi=Iu1S59vi13N`V8IBPXl!h>|3Ee93w4FOo!%i3|}|{xc0}u1t@i ze`X)9Vor}DTTG{ldVO53mU{Q2y8522t_WEZ!sckm{$8%s&P)IK6{-fagKD>-HAl); zlFX(XoV5AVPh-cOOM8E{4LG@Mv=Za$_`jg->3gO|qh2)iWwZW0ACtA~L`W&Q#gd*VI<+9&0gmp(eu9$UiU1oVw->brFab#);b2(Q#})gef>gGu>(H_iwG?lR+y82 zn7g$Bs?K*`XN~eGxLg&`2e&&$%D3{A~F$6;^W!!kdMquD+nGIsEA4nL&0{7h3=8Of?8Bp zXarQl^!p(b2bW42Wr}MP8j;SG#qtZ>JPc!g29F~x$7+3XlOPEd5)WC*7DlFO4NziX zvr{(sPU8<)>v=TE$kPX*(QPZt{v2b`A+u~&ZN95@FK{zsyIU0*wQ5duXC!%IN`xpy`pR6DT6lCd_oQd zqZi0MpQ$<4-6=P}NAOvvsoMs+V2|y}E)l&UClshpr@2$oD(n0#qIi^N5wg4)AK&eQ ziBEI0NlI;RZ`+!uwcCokuVOd$qCKp9*Fx8k@;y^TN2k&v6?1--1m=})Q$B0%od9U$ zCNm|qtIgt!Da7t_X4`pMW26{iR?bc@lQwNLT9N7@HBYY2{ek8IfV4e%I=^ zJdOmdWbMrLcN<#EDe+qdzt+!s>PXNCsGW0=Hj0sFb&feCMCMvO*ST~JnU4p%SAk#T zoJRxNR?Aq66aP+uZzX4Vd>`jsjz^E1RY?FHau*u4a`9Xyz<}Fux#TiZ7iU%oMUxTm(3df-ZQPd zQpSXulu7xB#~S@Sar~+TXdS-i^&knZT257oBV{mIHcHrTj5zthQ9xV zZWr^SVR(sp(^uUg+Sknv4%Foj=;Ad@{JyU`l}IvtxBIsASl75%c1X3UQ88J6X7%Dm zujvgZ8f-*jq;&|}bJQZ6P>M#V=9Qm|pAK>@e zi70w<<-m!!cKhQ}iBb$+KrL$(d2SxDxaUQ{fb*WJgE?QjvYbAYgckm20#*fv}9Zin`fYt8L2kkJ{1L5IgNHQ~2Z7louA&Ps7X5NdMm((6T^>to@&slN7g;Wr z63}V9s4kv92+y{2)qOr91>25YbnSbw+qd^BA{t-aw|=HTI#c>!d<4psXEKyz{bDiu z_B@$`<_4|#yEIvcEsw+9<=xH>D`lftQgx==c`XwQJqc!O&OI>mCLIh5;v}B=!ezbw zh}_J3EwiqP_8H7Cc1>ex1j>w)SW3RU?P_GIBg&+mzh==bk-s^5tt9cSiov$ZMIDJn zmRL*W?9|;4>YIc6lO54Kg`TT(Qfc&jS6%J${YtXCTHp7oL$34?G)8_ByA=L&Fh(bU zEB_E*_zPo!)22iztaLF+g+U#XFif8Q=5hJHh5(s;fJOD7YFmaxSNF3g2^l(){M()l zN)FSoU0A6`q2Y~^pVG-%Mrk#XpSIjMyt@ZWQhQ@swp z=|1yOUUNwpiXX5#`k=FO+85GA@Vz7UmF!cRW^`&p6+7ibj61XZqdXrjB8%j@`;RZJ z&gG_!MR&Tb8eznF%6F`ww%5@G5%iFBW^GtdjB0;lcFELiEwKovw@B2T+n#gtCfZMG@Hq&2XG_nir!1v8qN0;NPkL?V+m9Na z#gzU1_lM;b0};ue>2hObD@F+(}>~|;fJxl zJ1P&W`!4);ka`L3p*hslmpZXlz6-A}*D{h5*B9hI^{J`U6^&R(p`MLVd6}?ZKhY4j z5vbWy&$OCj)DSfHCOnP${>oVBHJ;5h8s>y@%8yQ7n;#c@$!aRJrJ2Qh(CWRj+nuAB z%Q9~HfA!b#!hR5x_~4T?xAtrqVVz7Dmx?58w|`?u;PUj^HcDt2Is3eMw7tgd!WqtF z7#?-W#O}uNE+eU4-csFPj?2a!6%j_D>gTYQsMmMuwf#>0sFqRRuO$?B~V~~a0JBej< zz2E70GAE|~X7O4ibhAa&x!0f@yDfbO_TJNV)1sE8*G9oLSUQCOw@-j8`_Y^j8?N9l zbQE^bT2ZIzZ;Q#R;vJxAPh9BE^~)AOpX78Kt@>GwO-h&Ry0@UZ&r~aG_26)9tgazs z8|t>{pCNne?RZyRYn|s?mSs{6s~$HZN_`k9o!d_8p>_R;frSOZHfL+j0|LXWtbE5# zY7|0W^lug37w|x)rX?C{bQCW| zgm4VIh2Iz5Bg197k=*5*Ledc@y+5eP_M?k5SfMZ3a#e$X)3FruJ0PBC1sDAsvY{}r`DZs<=<2M z$$O;8(zRBf^x*^*?zUt7SGG`&r@x9`H9>n1OMKk4Q>6FsSJU(t%<=|TylmYVZkk_Z z*isIJYo>OKAsBg z&9tkdgDT~3j6}-lIn?ZA`ffEnoDd;P?TH%kV=ngrHP}QepJMZn{oxPTV82DN4yk$B zj~95zO|_APHfb9Bu`S8QG-l&<+d2izNl-s)aueZjurIY@H*ES=U6cNk)ZVgB#JcRc ze$Ez89$1*J81BCh;_Hc=CQ!`O7-?Q*%}rbJ0`I+D&b(@#0aGg9(eV#iFfZ1A=Wh_{aYgYiHzBfK7Awp z!t`dtX73{jN*$^djgiQ#%nWjL28xJXAKS-l9BzuWh%Hu1>dcVe?n0uyNN=WcCc9XF8ukYu- zy}Pl+aBnQaymPZ8P#E1v=`*LlcvPpn+F++Vw~TR-@r#L&Cu|To)yo@kzZ%X@x37Hf zmxdi!m*Dtt()X=|IjBoP)%&c!W$?ud5Ia6!G`&IelL&JI11WI)JaY~6jCN@+!>6)q zg#y>R-mvwT!2*htqJKKcU+d4XVk=%vDm32@Th6b2=Hh}C+f+8AR^ePqU5sYETg_X^ z7`uc{!BGBdEifs?X$HbExP2%wrP57>uU)70;i6#AD`HLO(OL7z&i>`qx*stR7`fwq zy6T0+eH<#r;nl|a~K98rlGT0eY z@#0(AW}pHs#nxD_d1Xe&ws$1O6M7tD*LQ{eZZCi<32DeXOxYL$&G&kze!g}>HeK@W zsc)>!9XRhPu-9s**-~RTuy00IFk{)PwZlwnWt#qOglNWBpkLZ! z9IL{KhO5n&E1{C(K#iqwkT-0Q`k5rhUoB9Ha4DJ0-bAv7?b`P*hD2`@U&&>S&!su< zfqn@`iw88vgyPgu*!ku|!>M-jvR8@^hPPt%txF3pCsvO6QxMJ2l^L`5%0UajqCjY2 zzx@RQ!dR=DB^RzV!2%v=2fW!gcx>vEtb)uuHuxC>{&a*22ba)$9uUZ?x5~0$fT_LH zW)hS|p|r0(r?Xa`Rz!r7?-qTmSi9Dv;^aKChapn;6`QxrX@qPvOvvo%Y@^aTsibx{ z?dtdGl#wrucwZ)`v1#pQx6>QSg1eLmW6~dg<%p?npyGQLckS4X|M`{8jaijc zQMvM9a(FBA?R%(LAguhm8m|+JSfHuWAti2Qb>IA;9?E?w#n`7D2R7-e3X0)-r2+D( zZq_BOoqkqLq&+T;z5?l#^yNjw_uDldUhIYOd@sj=FU?V- zzL9MtYKBQFf2426HkLy;W9_EE0*xXA^E6vQQNxzCyjjIEaRN)vaF3s3fLWhK2EP3( zY9@}JXhrVTG)Sb3S-IbPTWE+)rgCmMv2F-Q+F=NN@~>LAY(S49M||@vO?-~)es0+^ z2Xyo49r_Hjy3JvmF1e;Ou)8f+quJPRvrdBpJf{r+; z*JlrP|0pq2rK9d(sCd3W`sC4WDK_uc(#Mg5h7hN;K+}$rI zYUWmr6ua9htbCxNvt1?$xvKB41ZCDqYZ3SPZY>;qX-;KlG+!9ai9*lT=&q0s8&_QL zk_Yu73yDnf`=(F4nMlo_u0f-?cKz_&*hF))ZSBqW3-@=qbR6w@vu3If_ZxQ~7?JQb z5^W!#Dwb`IdWzW@m#>H>Y?sN>@dzlirA{-lcip#V<-S3S!emS{fmbg$GcOl?Z%Sxk z^|Buce@gl#Wz0te4D;Nsv!A!82OSREdxM{_lU#Y=b=ikxIgBEdg2L+2SIMn=&$-gf zQ*~Vi-J)*Z6?uH_&JB#`F$v36mMUKpWuHE_e#}?nfX&zH=vG&iv)bAG(Y)Mg>GptE zdxUgi!JRg$lz!sq)|}Mt+zN@dakZ;COkPC~`C#w-{KsFBva!T#phn~~Nk(_x^CwwL z{^s|-b5Q6a@-=Yc0;iEe-I9aK5<~fmT8s*ZeY%k~b~}=ZFCEz3MWj3OcyO%53*zHniDSCW^b4pUQOpSeQ((KBAUd6eC4*ig6{5eeWX_q zkAX6YTHl@iiY4{w3HkG4&jb-d~m;H`N-Og7!{F^$?9U(5yFtUdqJuwYY(Q!6mBi2cT zIO~o7h@krZK%c$QPxMl_KZ`$VYhkfPeS@d|a*tPUI1{y9Pl-j_W1)@DNwIk^YOA-0 z+IB`FVTURUlJC3sp&a?u5{VY8G=p!@Il^orho4%J7(|W0m|CZhnpFyhXMVQ%b#OQ; zibAC^EP6_xDxCk2Ke*>5uc5J^Ctp!LjK?p@?>edsv4a)o%Sw_~;4-PZWKM>>wM&IABHn7HKVouJa=p|^k6X?@Ui*Smr9OU*AermY&LS17;Cg? zI6bBXo1E$SOH_xs(@80`QuABAcOX!i;Vc-(qh^fM)qE7+uK zr1yvDug5!gN?rK*V=;WS**bz5{h0zsNaKwor+4!C6+y_QL9pTWP4qpL4t*?jlLAu- zgBL&iETVnXYbZV1&mhDm}Ysha{*U)C~?IR_BCa(puVx}du$?b;RFhlsf& zXuuIXraPAqEu^rxDs2V*j8Ll}++w$vk8&Wa=pfUR$hzu#dhCCZUOPLoUtocRVyE;G zYLoEw)@}MPNQn8XCcrMz=qN&`qo63JLPEkJUg1^Cx5oY(Ov-71rhfJ z)^D=+&aS|I11zYD#>hWFFeFZi*b?CpFAUZ$;uC2N(GYhsG66B(p8BqV;8!pgg}R8_ zdf(=Gwkg5h)utO&c5f}Nc^wh{FA)Cy*WHJazft%2RQnS+ce3U(ibvd`!?{CxfAfet z^h@GafV&jHUEHG0ys-O%O|mRCg0ADSPW%$&fpdgaE>3#Pkr!Lnh;!T_p-HR*#8_v&sUZlLDvaBj;B0}BPlpXnNo(hh&z`)0PL(h zj_gK6SnDk?D_Ri(2xX^4AP)vo0Sii-Ap#SGQJmffqoU%z+z|rfzkB2c^3p++G=;3Q zUi*E=fIOJlZoy%||2KgmvT@%4L-MzL7v9h34HN@IK(dj1b4>mjzuJGoRo!M&#^Ffx z>2RBZ!jhJMquCw+*AAV`PQBlkaQ;Nb%xG21q*pcxw5flC2KaGrz4O23Cm+tw&^{|U zf*%<;Kl8Q&e#iWX0^m}`0dV~e!0~CDN_wBc!?CE^=iuBpV#~T6aZ?WGX7M0t9l?zk zoEz<=&gTejJ|j!0L>Fu5=k$?x3{$>Zz@3`1IaZ{ze$mlaZ5jC5U++HBr2#cSJA36h z1ODGykQWQjX?mygh<VPyY76-0n4 z^vu#4y*=W6={vGG)_CKQ$_$9Zr-xcZARMU3)3^oz9aIuP*XR2`i5dT{2>?8) zA=J9v4i21pXr~qexCh9cdED0J8_%=(to9#YfKWbfG5jyAD8gA;;4wP(zZQb-mkZP| z5ikozf=~d_Ba;L#Mf}T9fBB$`cLM*t6I{E2woXNC5{Sc^bRS{EXHI8eh*T${TC$tW zi=f?IP{RG3Dn06vR+y%ujfTR97BrPkd^tkhG`x5h~Em z4_99)%}!Z_c&?-2Xf=5r!N4k|6_mV1#=^|#_w?$*d21t6v;#za3p6z@#A1AjE|NFM+G%R`yAYHKEKf#mLSj?wN8 zX2kquNpr;FTR4l)GG+7;ESe&FD@SjmpErh@hKZk>)UXATkYa+QCHD(Jg5Rt-2hC0?o8 zruvIfFW={P_gOHz;~pQ4bvDO({vf5uO{7geO1|6$gWUX3EPMd1I-ow$%k*&wB!PBz zg6w&Lbxf8Oag31^P!a84{vaBmbRSXb(pz~`z?`I!xK&*A?HjI?R+hBeEgL25@osuf zq$w1Ln~&7}w29*3YdPC|mYqKiukHUTL%}#C-~vdi#u3@Kfs@C;Tr(l@`!;d>_;92s z5(CI%^~3?M`0XOSjkb>Yhmfvz>XyzB1PA+|qmbboTwvpw^jy)}OV8i&9k_{Ld;y*qmacOy{7wu# z3JBp-Fd&`#O|6y}Dv@B!C>Ce)>}+iRkU$sK+4_M1KOcq2%{cwwbHOneaMd*9lXUv+ zP2(2eYO^bL4)LIAm^RXdclt%=v8)|;^zAmW^Z!{b4Cs*RMpRNOlj#srY&U}&m}+3B z#&7lwoZ#;H;L14!xOz(NNE5^`_nN4fgYcn!p@n~zg^HN_a$(OLgApQuINg+iJA?rZ zpy~ycDCd5A2M=Nbt#b}uJii!|lKzO|k0?$VV%#6A_+u6SFFUBPEceY}sTb1(eH}zK zBUN~MfbwA*CfXDQwF9ym%8{tgTi$b*%WPkuYkze0+K0xpP7@{VsEkE7F_ zs)@0Ud3zN^O>cS+gey{eehCq<`B}U@0&ISUheEuoV5XPSEP15+EJ90B|B|$6$%46} zuM+YcXN92Uc=;0J0sE`1sN)RwR~85(j$Icfd|`0#+0kd%C!eaFB9mVRZY5h7sezyns%sv$RW z@KvpsJyhTN$N(r|ap%tezX=o;v_~mpSBBuUM41=%yI#~Ep)JJ?WBmPI!@~T!s8u=#bF37(n$zG z?z~)hGIvJHO9C@3^e^Tiv}HgEgnxGM`hkEdD%7wzHc>`9c#=j| zUdX$XR2=i+xJ3Dj97aB57KcMSXJ0RG?@85>oe<}JsAi}?DMw?XL}Yb6tzyR2a>ZTW z$}QiOsA*miZXTZ4D=&8oJFTOo?;eqQ4mtNe0IRYR%vie!nlv3kTGj z_KdO;dR#!!GkU2IqPqgKuT(wQYdds)l7G9(?up5@*WigDX+STf51*%8hk%^mIL=8U z0zgsHGsAFTiGJ(UFQHIyTTpef# zSOEpkR|}QVMi2=0?}JP^6_%KdFa-A>hi${FUDVP;5QzrCsA? z>rk!HKF9nC2@U75Xcgg)JNH8jiT3Z{rD>xdF#vvCm-o0AGvW%M7#%Rqlt3=<7rET6 zVLxQ6ofK_1@Z6QjfX88FVktLvKW(YM{gY5lo?+*M#o_YE=*4Z*uT3>t$9P!X`n6lA zmM_F~bchZG#e@#5Jp2WjU^F^fUypK)M5rJkO3sV>v>`m!Q_YMUcl3>GcIb-7O@|>q#ieiP~#P;#poth`@pHc#4|y!gY7hy3Ld5oy({v zVNmgRi-oQfxf;dFoo@E&je3CtroF9t0m+!%x_GcdP#Zi*Jo-)u%h1k$x-f+v`xOFD zG@KtF-4{o!y0L1Hpvl9*$;zif$l8|rx|XUwCWkt*I?j(V)byhB)d`^*BpD@hU%#vf zVe_#L+CfaUOBV{D)#4=d>n{~T(J=+x1)m^S2uh;oh}Xqrx9B2%v)k1ufM&zNZ9JD19u6_;ijXDyp?o~GbqQJ zB2Nnt|7sdtjS#8r4L~QMDZA+JYpSM^kqZ>gBOxb%*-+D!dUw{LHAR-=)oIGPLCkGj z(@_>JY`w36hpeQ@pJ#7d{U1}laCoqI=!Huu{95yHr?-K(m2Y#95ok(#yhz~ZRim;E z`1$G{Rdce9e*IV~Ey!AN=5*mB;%J0YUl)usLUle=$WNE!{0&PSJGU`n#9z z4DaLw?~UW^$O)?n;UR1lE?7Y$$pCLztN`?*k0*&XfdB%?{yxVm(iqnGUQR}oAJx!yqx_F?Bs?OUnWu0KkrEb z79Rjz$-uF>Z5}TW{o0!Ngr)#T^EB>fn|YFW_!VmLuy@Uthx?n@g+q3#Gmt`aBkPSl zcD5(yPbhOVT$y{XpdKA!#RbZI4Z+b#Wj27KrUwcfa1J^xmXCf-&A8OYziT3DKhvA3 zS+uv(R0AwtXcIOBDbs6>;?kb@@xEc2DavU&+TP2wpr87ldI{@2wIW9JwG54N?rrDF z{SAzfeVBbshX@7P#*qC;7I237s=kDIPmDeWqv0gG1L4rR4|=N3ND(fnfhW06u!)bC zJh2J^Z%U!@qY&U1k7TC{9|_lwA$DE%)qI^^FmN}2+wuQ{u<|xu_FSZJ#-Xp+8k?9* z2_UyrgrvwNdC5s~-Lioe%pP7FDzelG?NQMcB6}LTiEzZ%a`j&Yeu~(V1!fzHAgZ9(T)UfXvFZVyMKr?ij+=IgHg%cEvJNqddq0nq5z>dUd|xcI@SPGJ7?p zQae*C*w*rRO}C;ZSwY`3U%lSCA?yJDotbKfW%5vUJLQ!ig|-+z)$%sl%(5#7z@_sy zM{;B?ZC81m4<;y~>jm0p`VkLU_dB?CqYO1bW~6S9s)4-@Cjh|5hg-Ogu_W6dAl>f3UYb3Oo=x z6Q@(dtR~^vmTI2-|G<|6Jy5-1 zLVBCTO?2#|q6TrDA2FWe+DvxC-gxe)FGB_Apz~^rXWLlrBvILrS?NLqJlA>@8W?%x z&~cKJfnnTjT+lmmIb>IX8eR|UL>NyuJy4GYvZn$bjp(c{_n|EuBbTkGTQO4=(l0So z?pftfetSfy{*-l(B=w|^l66)^f@Cz$0~N%o@y2GDcXgWioD!2*9G)P9}% z*CAJWVL&9q-xEr2f)vLVw6Sq$l;aGm3MPCHoX27h){>TfiZOWBQ@HVh43ST}KS$T| zMk8*PI>?cUILy>?L-RryG|4Yuk~}TSbT<_#opDA+X}kq6qwlw=!_SiVH+Qb*L5&0n z7*2@?(!=P(BI9Q5e^UV0AoIhCU@0%~L9mP^j4M(+3waj%>%HDZ=gopI7CoxBL_Psb zK0H4obz#Lo*lapTE_$BaZh7?^KIzXuZ{J8uQVL$biqYo^a;FAZUgiFRc@5ADA`O@I z?kuax#wB>t%Vyk@cSk9}-L$O#!yP$bPI#9#qnUJs;g*EqMv#@CjE?ie7YwJt-Qh@v zT(AuboI`;47sS8XkRKrz81h$|(rNL4GLiAwcb}kvjuk>GEF#A1&Cxs@&a06ldwY&N zeId_h!6%FmcjsRRTwtiPUPN%Q0OvxIq2)D#iy$zqsJP@!!RxQt&li9K7qf8K(YR(O&XMv|uhWkPkmv!}z_mmig$20{b?cJ2UYZJngWu)z(qqps_##uG+8wcX&nfIZcMU-pc5vu1|A(hveU|OX0CUCbG zbO%(Sr)}$ZjdJG_T3@1%Aq_$ZY(NMKLZMxcuTvmPfUpm;nWFs-NKIe1P7O%=Bk_qe zKfl!;*lH^T`jpiv$>W*nvS2)lp}Fx1h`FdSYOAZN1HTC7ckTUaC@K)?d1qiL9U2;;GVxjaBjgpjqsnbNU<@gOPNH_kd z<0$|TE*fXX4?wKnr6%;7zvO2ifZa8V`HB8i3GTD$>}h-w{D@ho_i6<8g!(M&$!hc> zO?&ig=`<^hXthY~f4G1|SZJ?uv3yg5*$kwqVlnH@;Vd=DGVzzDrJ}jn&{x^XHQMin z|9k`DuQDi_o2$HTBLc?Xbl6^(ZU8mB>y|d>V%ci-+H5CY|H1yQH_u`j3(z6@TVjRI zuBFb~E1*Ykz0F*Vz~<&@c_P^PAOyz0_O%X?sZ1yV@z=M=ka$GF{jYK3hdbK9NIfsX ztFFH)rCj~C&+>(rrwa(OTxJtOtCvP=YC5F^qW$cbzFqX@vYvXK={!mMs6OzvGrT+p z?hI2=-Bx~d?|1c*wxo{)38IMx2^cCc8Cv&wf|H5{YKl74yW4y0vUU2Igz7djk!_lg zaqy%$BFP1*5gq}(8qtNzM8CF8{WIk{kK^kJkOYTOfjti2SVu%3SptJwT9GScmqpwK zThL4f!^9`!1NkqF*9T&ECQ67M45lh(E>)e!e(iDoVhC&ZQ6UT545@tv-)b{U4JZwU zQk>G%OWC79>p^?=XF|jJ?qa1|(Lw>_!AOORKxTI2A-bAuO%!0R7PZ9lm!>qnqP@^* zMUoyu_D6y|;jQ$SZ3>~o0KVQC-U30k2h9L4(O_y%5cA4tCO~OJC>%&mHq{at8s)S# z>g}zZtLMq!GArh3y#Mn1XFm|K&oVI&X0#U$nU|BjjzkJnh`nt{h7^c24>xN8q>DvB z^K0l!=VkML!OJLkr9K*AoN*@#qEf{U)3TWSs7PJf&Q5O0@wzDfKD?4DmnyRZ$ZI!; zF?p_zafWKTI;@74qL}z@G@+xQJ*T{`73p7EdgT;J9!3NPAh`fuuOnrWfossVSG{Ji z*t!Pf1_~c6m&a;HzXVI(eQmAk5^@|sQi{e@wD(7)C0N|w=XiD2`Dj!70dE#xb%LQzbt3#vW{xeLa- zyQk;>E7mqo$ti(^LWrptTP?bWwRD|bsDq)bOyONQ|}Sk3h3>PJpiuJ@?kIQUtM zia|g%4UF`5bF6^Cd~O?mO9l|c%q0u{v#s(A2)L16pP3MN$WwvP^{7Smu16Z`!L=-Z zUGZ?{TjF7ipQL3jH)y!+uZyA;lWKhgG-Y)?gUFi2FfL-8jM zP|)%KDaM8;#PJspWcUV``xNH3P0OU3vP||$iE{S)+gu3>z)TW}OLe5?uR({yyA)QE zy3`NywX4*UQA8ym>zH>2$w zvZZPa&+rXsFGIh>ucaDnY>0WK%NBsN8kB?A$X&Na89`341P)M7o8`8!H(*`JtUPdB znJz#sSkCPMy~XZS#T=mpxYyD=T-Us<^WD*`e7y%=(&`}%Z`g@1y;Q)YmzBHuuL1|9 zaT2mJu-dNW?Zq%AooX-=#93hv-mmLVup7uTY-!~I1zB+A?n-E7nP@NQkal{S44kRd zKj3b5{6Z1mN%^nbg>zb@I=@gjLS7d@R@xAdx6oa=Iqc-^b};RRMXQiTGw%+8w`Sn2 z40XHygl;jn&C(H1iv1Z8ZD3GjpSMAlN|aHtGe^}982|-8mL)e>1UT)00B0W8FrKgP zu?O)UY>kfsu&_XWeOK84Bpsuml0jc4`TC z!7T=~p-JCfa2&@_hF5T$%ir;Y0?GLXuE|vus3?9eIZlF(DKWV7ZgLf()CA3}jNzpP zUxv^tZJ7n~U5VP-TW!OJm=;D)7ltsOq!M9KAkldOM4-4Jefi|+70UJ%)bzT>o^(}0 z0y>qe&mObG#7p&BA|~QNr;ug7Viz|0gEH_k+_F^7%WbupGb?tCI43{I1+9)-))DK! z);w4V{iNR(vmm(z#E6AKNPiim7nopG=N^g^>7wmV(c7M&sg~7WK0qQ}#V%khcIzw+ zv5~lr-Db{VHWYSGr#_$L`Q(uhkZ%=>CPrqTYjr!@sU0`>2@5`r8vqJno8 zl|U46P%x@G2kXgt0mVqR3PXjhUm^GmD-w*mfC5azgF52WEysd+4objC2aKyH4ba2+ z5@@~0y2zX&{^%aw%KQsQe@_1p>;-v1shPov{xb?%*B5ufJfoa+>eEf1zgZN-M0??? zdR~GR_1~cQ3!ERIz01BzKyxbiZeIm{gMat)-81ib;cAR|thT3vWCjLWpWRS9+DUwq zMUfZ`x}U2`ygf@EBsfcZ1s5U=E}gvPc+hvnAVHm?lRkCp%AH@R>CPLET>W=-K|*G`4iYm=){d(uZ#f?Hx;2R3&8l(d zPC_*y2Q+4T-uLU7_x##UXFDF?6!0h|KwTiVLX;=*jiW{_i<1vFWxI274#X8kf>o;! z5`)io5C=}rDL4f*I=oO2=_FJ?l7Pmn^b^V>(#cczRIRESpQh^gw2eB*u%TYUC-D`O z0P37d4Kb2EIS1m3E_e6oLbmpYa)<+`>GWs@8qH$vfqXp2=?l)1$r{TFKEf&l6tyX+XwlGuN__XM?n!1)L1{{iO@IRAjaKb-obIe)31Cy#Rkoi8G2Cc;)p+i~fri z{n4C1n)4qJ_@g=hfWSYT`r~u{fb$Os{KKjLmF5iK5wYks3qLr!O_!BYCBZR7MQNao zGxNorB1W>h*zZ&``RsOF95cLwXK6=xRtN8^6zGEPI3|wmf&Cs+NrZQgcEFw8|;haawRU!db=UfMjzMe^D#@P>GyMx?I)>8{krw2Sj zS^Z*rV2Ukp1A?yXso|~pxprbdoG*_lL7ppfh%ZVZi97~n1uKL zfuvL8HNy8&U3e;Vdcps1$c~|W3ZP8#mm&C{K=}$RvVS}+uiwOafc1Y>S`R@-AfZ`h z67XJ$vp_jTb=+let*#&&)mfLy!~5PJP33GPK_A_EfJpbumi~#JGJFtLptdS!Z&3IH z-H?`_A-1C!)X~<309pe|+D6_4V$8e?rG^h4~YC|7#@rleipbzkd>! z|Eezk_vL*aJ2-ttsp)e%pY=rJuu%B-mTI`e67F32Aqw!KYyNsKh!onoDy{$zE0(U$BK7d znh@M<+o2bX*rN9IH{t9puBYajyAN(AFEI%?$qPW? z0$LaRQ4~7c+`MA&$TRiY0;Di6IJ)~5AcKCnI82ZQR74p6`cMM`gcCRmUhf6e6q^N( zA<|FyOz$G96jV(=4k%p!Pr9grF-`V!i+1Pxb8Fh%4ptd$=epo;7P$8$InwD;Uynqp zL>@921z*q(@%+yzCQgnF06ViHPo@U57|n&Kpyx4w;>(Se3I^U2fh+R5F4_du^#8DS z-eFCp>E9nvEFg$VQ<{p3NE4|NnmUS#6h$e9CMYclh)D0_fQ6erJn2Zh9OMbSx2K_ zT}L{T)|37rrM&k191KrRTB7~p8Hr(Zt&bgq;rep%vUY!a1>PzSh;&6Y_Ud=tH*%dm zsf;bwWecFyV_EF9*W5kFd)!1t_l1Gxep&ihj|KguU^sM;^+Pr?`c0 zhIwqi!Rdv`NmFvR!6|aRv}PH>-X`^6V+2$t98UdklmF==9%Epa8|13m{dkS%)xq8% zLieM|@SyZvFs9R295Zn8mrs#{atzA|W=8c#8e`xq=KptJVLpAu3BKaSDe{*NkdQ@$ zWNmwP5cc(>r}qE$w*KX}OB3gD9es6!n0})Vz&$!dY*t++jBSpL)1=Q>y$P&pOQz zmCHlHm~DuD6`6Fu(|trF-hJIMuh867eMzahA` zT1sd^w^FgvfVNY9{vGMd2h?AN!(Dlj1^!_eIH2+;t>eWWpD;Qymt4S&4#Yri zcOW=L+1H=EkhicxNt@CtTlglY>AI`ER4MLxd7C8101v-a!M(*IE%~)fQ^xQ_o+aO@(z!~q*sg8a4>dbw@ z-Z>kdLn{^ORDdEp{yoB<*KUGKOZg9}L`1Zh3HnmQ2yXjb2e6(4j2$wT+lVVJ0*}GG z;9SGb{q>-4q8kzWeHP$f&B0MftZ{p_n8CmK>;`wMrgDdoj{ZBWr6y{d6f?2P#H7>1 zu`|!kH~OZ3=w7GAiLOGYrChJo+`$&p!io|QK=pw_#x4)krjMA5fx|HD3frI&%Hz{INQ-kB(mlYY^E4Z;U2}6Mkz7s zk3s4D=9%4=XF&KZJxUW@2Zm*MU;ElRG`w4_3(3WZ#xmm;(xGOrF!X_E81NAv} z)$QCid9Sq8JBw$xDleVd9kE^|Te1tRNme>=lRy?F{yU;s1UQHc(<44GQ#Cv&R|SZ1{TUG+@_^|WB=Xg6y94G_MmTf2bnGbDJR2UoH@T;mng zSI?Cf_`c~}@X{ipk`Xu;aNhB8;OxUwZ3tUOqdu@0>uR&(1Huu(`C-Y|1tgv+Lb!Qz zt=-5?w9|{WUXxlVG00tK#)_i8+Z&wXl#au&*zs1ePQw;3*1I=5(y$Y_9@c~{WTtDh zcCU3^go>5vIk3xVMsYH`xf~R?;d5fC#d#&VfUEfZi`mY7!J}+WjzR6Rd^4TISgxp` z!ahE`ie11e$QO*deL9OWu*^aqNN+_oC11@kx(5!xlkk6ShrHYjj&k?osM7#@M?4LP zIFc@I-nWS$k-ZEDDIZ@O*DXDE)pL0${C@1(pdg;ww%18D;6@x*Bi1#WGu<%VRDH1>pPhys-^@xXAD#iL)^#}dR-U9^ z@5Wf{hESAq|3iGe;FddPmU3I(>89vCdB&Ia&=3D2hj6Yx$4<4qZFr;KU)(QG7&DK!oH;O#T)$Q+X9roCS|Cb#tfXavE(V?Sfn} zI#)gJR|zRMkY)K2d#ZBoB2p+W;zmDbY2F13VU_)EKCat|fG^1cU3P9m5Kon@vA-ro z3FNqW0+V2a_2m+uX^nueD|GW!v)hCjkgsyL&jx$W#ocVLcdjsL0|hYe0uaYu*D6pi za-Vq}Se?^u6uH>|jgsG;ZU`DJIS|w_^;PBBV|JV;uxDe7jCTwN@hKLtigm-*4taSq zN?SJ={0`#zNkelKW14H>nd)BKPTnP;*QnL|qt>dTJ$IfQe5~^!T79d3A-;;&Z6Gyi zMs6TLvRvG2xmNwRR?$_~O(bYLT1k6%$$9dis5{79y@6h_tt0j@!Z@}@2t`a~EEZ9B;;>}2dD;LWM zIs0ufiwiyCle9N&Pc`$wC?3-jKZO7`He{nH+ucEMgOUCZ3BbR2kiqL(ORfQwEDqgQ$h&EPUZ+EuYwuJph{iy+XUevXh97BU69!VJ_8ob+IO z=BGTARi{Ii-PU?Sc?f8X=NOhl*#fmJHwYs<4AlvXFS@8(xw1T~_buk8t5aGVsC*Yn>4KHBUhWjr~J1&z%( zTX+UUe)cy;8($sRa(&>kwVqrI`GoU6(LdzoT6q?=-gi}tu2T)7u9Z1XcI5QUHEZrf z=mEp*#Q<;Q0HLwrmB{N`0tSkLP7XgW&!M#8)f}H00!8Vo)7#D10T;XW(d5*EZ-l*9 zUbKx1h7y(tiD%uQ!b_P@dNXq6nH%`-&=%K5oif53uWRxTRW3kidRXG*&-6X_15cBj zGT0UVi;pYFw(yJWCL5c_pBO|{i-sEi;WlMi0#Yfa#xy2|M19AXGy_NHdwCz!D%hm@s`93 zD%mrZqERpGz3+Wp(e)X`U$5^oaPIT1ktYIJCPWRr zrPYjheBar2Q`8_wq0^c{*@FFH@F~PnTIoFA)|V?=bn$=*+UCZ=okJ@ zLl{<#cVw#xHag=$rTiXqmrt1QV5&Cz`1rs9>iHso5O74R3LTgjl>}r%x!z8NX%!@7 z^$+TDKD~pnnsqkwR_)W$M<`j%SS3Yw;FDe2=)5D?TSX}W%+P&=D8}PH{T@J)j-z*p zI%Ny9I?Ib*W3-J}MK{N+boUyjl1+kbuIYp)pV%BGUz z`+p4Q@AJyidZjoFqg zn>!=82>|lMPGDC7+u?gHS%3=e&v0HTIB36yg_prTkUup}@4m2V@og#Kzoq7ulI1x5c2AqQVI|aN)Mm zJM^2Abl_8Kbi(vL)(41%iwEx2H(bo&25JzBx()D0n)TuwGJ%EeRnz0ZT;K%xa;wc_ z0-pX03$ZF%dkV^8(@1FD_e7OX$fY|_kR#Gwbqlt`|4KE&RNC!`*eg|=R%*VeQ*a=P z&G>H9SznBN!MT2oimkkNwF)tBqF`lsP|WWLE3-=j!S{h^fZG0u6J<(VsMw>q!!Deh z<~g7I3Y|+3fag~!<gwsonqLFaCN_fYS!$L2v8x zkla5*=1%CJY6Fnw0=(i8*yIb>9;)o4BgellT{!)U-)qed!S$3^avJj>3FXzJTTYn$ zB0(*a1777#S(5Xcz3Cn^m7E`3Mxr?(m@>$Y;X3AD7{=$&a`{H(BX)#>aUoSmdK-Ff z%K<4SRJgnil%x!qZ-i%_vp#IXy}DM9n))4KqUuj!+ERo%R*Z^W4|>*9|$)c;w~w?_asp4SUsoj@gKTD zim!$oV$VXi+f+J4;D`IVqn)}>PGmQ0Ae^xqfKX7=nEb4f^;YCzeE|7{3Hi<<=?WD+ z5rsg`GR&$uG$^Ai1Q>A&+h-&;d}U%*Q)&6oH?!AQ?%S7INWIv0$E_Mk{$J0Bm#z(| zYwSrEjh~#2hj7~nJq2r|3u|Qdzg;5tZeHKZ<0BgAxTFDMuT{iV~15AEz3zz%e7%~qfGhylXFLN2*qbbcRlLJ zWlVe<)?vMU`+?scJUD#d{2}WLZ!)wuU*7d*+wRrw4zbr^e%F69>TkP(e`ERd%2V3% z#+n87^WcjY7UnS)Uq#Y-PAay4o?Z6{Zy#B`)R13Y7>lHGkyZdd40^an@El_$`*3*8o$l(M_FfXv)$m6oWpQ)$Vhz6{qf0=O%)hlj)7<-YO8p@p>!r#N4` z;Z1PsC?HxH_8dqMO$FO-=TY|)jgG%0Kq)l4heLZ=7;^32MS*ij+l^-D@2snBIPCyHnEmj0A83IK2oNw z&0;@YjyqS05cU@598kS7!>wW7I3=eyR7G`eTdVVOQy+#;-Ua~g41`0KU^BvfJ25d; z^k3)+F6H~ex+Xo>S8}eOPM495hGYKHo^fs$0Ru(3^4Es!Uu*f#vSHaSgjWxPEi-}p zXB10~+qJ*(4U~eJPMh>?f8#v~#2Dbx@=4%2vate*+~Y&x#xV{raMTMvwSXM1V3zHC z%v=Xy8Rzt|!>YNd^l4T1L2`+Qx?E1zJ}~^j*xt5};mhj<^6q;<`gj ze0@iRH!eclH+(CQd+$Bc9<9OoF} zzNuz8s5Zw07RiRQOR5^OZBF?A^8Ny$_WHbD?C|rQ=Jcp6=m9hDDtT=5R*NzpmL| ztm{#4SVlTlb*P^E=s-h;Kwl>eptRrN)3eW-4t%=0p8?Cg1y1n36`BW`P#ikfrY>e2 zm}vfxE~Ak4C>EKXh~;Dbi<9C=66^p4>`oMjZQ!Xb8S>r`4RtG@79c3vBC-@XOXXM^0@&~c}{!}v3g z?&D=Bo6bB0wrgbH&aA^k96oEVB|wiKgSC$AaoC++*HCkmei_zmIPT`p45gNxhT^NC zv{fwBLeJl5_S5`e zx9ljXexM2Bw-!IC=AuA`gJ|u}Hplcj_7mzFiDxkCzhq~uuL1S7_Vi~VIfL0w_;&p= z0(BMwk(;MT5yu^roDNN5AV#)>%&kuaY7~zAD)5=OiooO!#+r2FBG$ z8MY>GxMpOB6*kpgYGQZi@v=fF>#H)7QlXHmZshCN6Nk_3mS=jI&}i1Bx|Q7K9+xx~ z4acXmr{GBIV9;KKH*JmpDxZV$_cHWzp*d;#AMp_?ev*AcW=|?s$|^@pEAXQu?aZJ|~Dlt45pRvTR=F z#~{>DQpZTF*9JX*!f{JRQ$Xk1`84d?Vgaz4amd01IJQ}qK$r|(*{#LztajrBRG2=? z=MFR41Cv&q&tt9$;mVi+(}*dBtoJzgZ72>`CW8;5e_rf5|7fqId5c)&X#()d)Amhr ztU+7x79A0(PA)_vaWIpvc;Q6mJb?yLSOpX(GjH41L4>$^Gslu^i+XGVOR3cwbWC(? zW8&!vHF9EX4lwP zA`~~+k>bYtN(U+W%@N?#98PT$QSbo++i@zDnqpXRx27O6zIW`Aa4J_KdDzgYiyZ0=HV{g;362xjF?e>N6#dy}(E4<+EFOHSmZ^eM*OQzSZ$0kN)dhy#ET>h+ zpY4*zY@KY@ZEM33DMF$OW<^0amprEdZS$;`)=Ae21QJ;g8r<4Uu-E6+pf^5HoBc#3 zLs!hYNA17_0F@1lcxC;8+SyGD#cw6X@(d--P zFH>M0b2~ap;jPnuC%?C~m@*aAVWP90zPjK=H{|hT<3y@;ww@0oQiSlBb2Ww9HDr-A67aSm)b8gU1b!=Y$p zJlPIf5Ef^+nTYWX@ILq>q}%{5iPuJpjRu%Esj-QeF{o;AKuzj%d5#e4iw=}cR;8jE5xOxx*WKG}f+HcEf z6v(G2PkBlXOTg2SvmH+$Wr=>o0#OXcqS;uKpyZRqlCi8%BNHv_tIo~zHC`hPrY=68 zZsjy~x6@f1rk=0s@LFk(HC>wStE19@V%I=VtQE<+^k(1So^-svfWtfWbI`{62%&d* zH*EFfNXGpMHD9~D>dJ+OVfgV4MrFkBC7B9-RxO_HciEq|GPbIodyfcv~5%Cgwrm zD^nv2VCkQgIw0;d8wivQf?Ad^(nA)}L(jgMhx}=(BPc`2{YCi`e^COFUdCz)!94?b zyYwNXVwpyQEe6r*+S7~RqH78w;%9`SB4XGy)O`9{E=%E~JyW;q#5*CqX(Zs3LD;<# z5WVTy7SDHFO+F3DvkXG`Lq=XQ?cb)Rd#w%dWANI%+FEy4Cv$`0Iz*9lSc-dJokKd3 z%ptW+SEoaDi1)W}I>c};L?2ThS}+r@@d;r97Z<#7ds@J?INBs|<=t_Ir_m@U8~;rN z=|u(Kx5$A9UP~YNO~HNUL|ahWXd}urIFE~)ay_y(K9vTpFp{AOQ_#^C?FK#l&%f0z z*GK>MUWgdAfVS6dURht6uPv`~D3!$(1Q>~N`}lQX7%=|Y@^s|>yoelmB=E|jYQRat ze^)y>%8jH|LZN}X_iNoi<{Nahst>3@qmqtXl(^%N)@>g!zYNA7f4?RQLoT>$_j0~Y zL$}XrDAGvS!Y6{7^IJ|O{`I%q&d3mczV(`I!jI!t|KS3jMMEG%3jOT2o%J0nlGq5n zF=O4|h+uL#LR@@Q-h+!}I%oCz2>hEo;tC;qu)PreX+e9?KIvmfht`38k?oHRAXN!| zYCJiA&reL#eFC5#A-h(3(OL$B~hrhbEzx>4Z zZozx9l%^>k+@a+4WT68FX@3$}PpJ^eh69(Im!`n#a3GVm;KX(wfN zzw^eeK7%nQk0>>(P>AY&ebHNQ_qh3bn6Yaz`tebs&&@@xbpS@vRVhi;2 zKE4v#y=wz`2P`ULN+lI7~$|`ItDDcnslrc z(Sb!fIF0v13dB*}n)2TQ_3wt00X&*0>&++eyVBQ{iKn+>n?Fa=ma6UMUszPgC#Q8M zF%k}&ptDREb7M~SJ|At#FiT!&j0o>`9*LG)giTYsX(u5`_ztFRuKqVzRC@AWK432CytreCgS>KEbjGG)4kuZ=xe^8JT8qP{bP1`Exy=Y=vo@cfv5E zGA>c#wAt9_8`MI(8D`lk^~Fv!ir^Kz$+TsAwQBX@u#flSx}O)&kqhX04fecMq)qb2 zR!k*m1Pa@f9Nt)4ATyB8_{;L{l7(^%j||p%!PT__tIwD=JFgTd?kk3xUH-C+?TvlLMET8 z5fsXd7k7{ydrpew_4_T=XZGOk^26LfuqN%KB%jALHE%_Bq!MMT*-#a)K^W@_ld*8x>-LC1OF#u z%!Z5yt2{m%0i0r4ECwF8q{qxqxLr-4Y~_Oy6vCVnZiL8D%;Z(gh)3a&xT^v%49_6 z%AuTiN-%c!l*iHy0(I$$MYT^Mwd1v7ws|_tJGC&tr2u`_qdI z$SQJ1cjp4foFi#>euo$-!=B~ceI>+Su-cQH?erJcg{usT;(Fj z92BG`()9Wwe7ss2Zd}!(TxRQ@FOOM} z3NdK}>$Nv(;Y*bZWR9S$yA)_s6J=KQ#D6ylWYq7t<%N{#USM*i=vtFY;0HLlXrol;H}gW zMI+ZflJm&q7^z5^bkm<$Kn}_v8*QzKz&Hh-wO*g~i?$pX!87y{2hmG^J zYZT?Sy*XQ%==~nzGOb@#k0NvUnWE8wd-PPG`15wZmIAWtND=uf@CcIAhBYqzCmZw6 z{dW{;|9=q+hk@{wY-tnJ2vX@EiRN!tk9jJ3+qE>#lz3US>Y#u2gz>U91`0YJ4Fj4L zo`Ni&7BYtXxGR3YEP%u_nPu-2)V(C@i~2w1P1OiS7H11#-8L@VGfTh|k`%Qun+Q0= zTvDCR%rD=bQhO;p2ezxcE-Mf1DC+Iz%{vvWe&s1%xu^2gv~@W?m=)vZ7fzY0>#1;=9j@F{>b1Xq zy|pIiRY%pv?-Nd1v0QgMNEvI7gvE~!=3B%((B87}aFHf2uqxtfw%D0FLVwQKbNQ@+ zGj)1tGL+MN;0kduWi~ZnNLNp2VO%no>(!buZB% z)N9qXA$+@8Lf1mjWlqCAkdR@oZLqVY<3DAh?mmPVA?*AC>^;E3!Ykoz$w(gv9~JD| z%qZBHNwdZDwMp;!i8tyOpCb7AS<~y5_mJWLTv#9KD*vxj&3;w(UK;?M|85KVZ`8@MWwaltLt}aMR>bpdlY2! z+$Qy~8I`8P39(h+bn6PpZ3>i>64%b-u2)k^+pd$+O4b^@OuxpJ2GiC@Obe>JwgmaDY z?0-Tm9qG&PN-NQyo~NXhJnB);J0`~E;QD!^eT4MII>c;{mT?(|Y@S#rBE6AjG? ztZEY3dP&P!)v+W&2)D>{{=HP#_M(P7!g&XYC?UEOYRA9I) z7_{t=Qdkvy-5?)QQkn!tXO2%^#8Y`bvOZ9Vt&moaYNy_6vxu$C5Kyh0+^PI@o%(Kl z(O+^a&z@Sb;=9G`@#*bMX~lf^JQd?*%{@6RH05d9=q9Ji3)Otrxl<=)r=Q97 z6GO%wywwMcJ(erwES4TVS%{5k5n7l)-nf#&(~9?A#I7m&#da$5l}try3~%LE+V2;@ zWmxDre!^{h#shQjQ+`2b$=VnGINFWU&jloz2?Ci_!S`;10?y>pi@Ro#<6rtuhmA79 z{L^(V@}J|&AK?u$+)VEa-3ZaBw=cej=Kn2xe}v2pY;K{~ARRw{{XrtS13&*lS>2y{ zf3Yr1gIC-3AF*yS!9^JNA{U}1Fg~{ojbc5PLq+sfrzIO&s9q4s4JDxxsI|^ljX)o~sz7v0>NLc2l&x5%%hWJO44(U-p+zzCz|&+wvBt@4=~e66 zB2g$Vmf9lOV$s}MmW16}=*%ELUsBf{#(;$}C({taYuqSfwM_)8o~hzh<(bCfkbxD9 zSRfMYM#?+fSMO_S^_)|eot%?W7ziU{V*~ZALZ?PL>jh~`avJ=dQ173nPRxdA@Uwda z^RYql^w{40!|jQWOlkE6eKPJ}_HO03KVL*^-`ixBSZ`OqY-YAt?Di|;v0xqvIh$fT zgN2J}N|2FFH#N+^-H+~RC>`$h zoGK<-CFA>7E{{}zA=RFWhF(>rUhcJz>}yZI1!9!w>J`-nCyS?A!Z2Y75-r6YKJI(e6^LM;0}w&esmp zRzg{LX!pgtn-pBQX4-T90J&bRorZoc->t#rq#wYebd)=H27;93(tIVbm`!r)Z{o6g zh8VZCpM4=UlM6xoWl|av=rYkfDQN$8do@Zx>3F}~qOPDtDXlIi#B`)<(JBO&Un*jP zrm|;y{UM7{xdnltj-y9(ss}{AH?pAe?az zr&jv~l9ITdjn(SjGdlxsh3gl*O=}Ob4fe*V$u15D?6sQ~N~o;0Eg{8w<0RT}V^N_> zMrArM49aT@afgHHq=Oz!?kh3mAcFxE1f)$0H#B7eLo}6T2}JW%*S?PISU(FMdy+%O z;>LZcg4Sd)US7@sdBxQ=B)-}Zj3SgpF&l66rb(J=(e4dYDl3IJG!$by@lCh7<~j8S zjAOkxESOPB@tv+`7DyU}le|c#c;4`K$Sv2Y1oBkNXm|H=?VQ}kyO^1(Ybnbm{C+ocZXyR3en&kjvX&R9jWeD2)Ho@#SiT8$@fD&)xsNR990VSEvkY zd(Q#-lF8F5xqk_q`L{(@*?<6i87&&{gn1J|V1=LeOt8Z1v*;JgV+nSnjm-mnP~q%O zZu6cTrlj#3>KKbMMLYIjoAqwww{2~(X4GP&Bd3*Y$eVA>HaibQJM|}q8$H?@<9_U0 zN+n8odk<$Ho+3?=k+sP207ZzhiQC#^Nwcz-H$)4z{AI~0zEGjv@TJXzyV3J90cxo+ zT*+xoZ_m1Yl+F{Kh2e?S5Ou->BxJ*qJw}LM6S@WpYt!@^B?l!*i(cv0DWqFnOEpK( z=a4|RZGA@5G=qirPD#gffUNrR87yIl;1XE;b)_cbL&Kt_qZ$&8KS_w?%I&_3lUXQ0 z^F6>3$Py!NLYLh7o3>gUAQfnipCd1ho(hZ+2waD(*7UP}f_e1q=8$9*u3)E!8#TtE zm-qPSoSftI^tr-$8Mk!CyFQL^SFD*m&V(#O86bllndp#y=RxLa0*+uITdf~T=hL; ziLy_irU~wplj3t5ESZ>%uWF0#Ri-VUs8_bIG;nIIB&9{(iGp)LIwhBfxg-Xki)@aJ zX{q_s)n-|2OjYwgR8n-TshWq(e#c%3c@D6$6C-f?jl8G*dy?I<@5oIomg)cz1@w9k#qE_5u z7E%TDsDs)nxox9x&$pNbPGF)WOHEvfvMrOTyR+;oFz{5u;^rP1&a0jaiOLndbj1Tx zUaQBsO9#Z~`GOMMsUU(FPD|qU<$++Oj|C!{9xF4%AS_2^A+jnPOXqUtRVIa(@~u8`%gaS`}+?T@+C4eY-J1$ug>{I znAxdY?BT!9%pJp%2)KZGalQR0` zDHSUi{9Xz^!#pK>&1t}EW2Ieg=~;9CkZcbY&c5NIge~Oa*PR4SgiRtTZQF@$x>mTY zWbu+Er+|&t8oOo(UGqIM?CI;()%=x>r&L=VYw)D^+Y_SL*SD}dtc0HT*PXwPd%F!) zj#O2a@;wPuJJ7N2#uu|b8@IEYcYSArthOcPYfl%CV6@k%1^aO=w_)#% z42wl-Q=(aQlw5OkV7ll%rH`2&`<8KX!hlD~#WTUeX|X)0HTPh1 z+7={xup748WcKL|cUH!C%fnQ(IInA5d3YpxubKEs*;OpX237G{b7Plnk-Vrp6w8tF z$=co3=m^89q3{i^^_{D?+UOR;6x-r2-6?3hjgw~v3ZRN4-8Q~2JgD$++jyo; zmi@I4zks6sU8dxB)A9+KvUkT=jJ!oM23P3}fxp+H{^BDKuHdZ)FQkI)39wuZwB=T! zZDa4!C_n8Zi!1poXO%dCqT@C?xBB6J@Pi&r53$v*?-}CYTaWQdPP1x(pifm3*{wQu z>+kGM^KXSEE^2h8=6`oA-6NIPbF)ivlclI{YnQ80=6EXfPlvkSA#~EK+$!VaL3NsN z!S`AkQX(|X;m3zE+$u~d+@KFpUMVxQ`;u}09OGbWR=1yBU#M(VvUn+%AitZXjAJd1 z5IpN??~#)IX)$t5U}o7OwkrLUYUQTIP9yGk& z{E@p2VApN-H&y#{*X6*9h(R8PoDN<8<V+N+VuY@ijcjG#y1m`=XDOMCmb-1j3rfhkF0c5rIoxzein0iye+G^Z{l35|V? ztC6L?5M;!O6&wlEHz&Da^zO5qEY0(Bq0Gz8FO}#ocFaZDZxD&4x;?Ijwjs06vor#^y;kdJ z``^=T<=^$8zF@z+XQB4O>|DFi!#ykW0aCgNZ{;oQCrX@xkI=M=X2Zwq)-Ed!1nMT- zn)Mh#)mFBa?%jiSA|I$P#JZ=Xe>pZuyC46nE?c^2Q#tWZ?4Sx9IZZjpdK6(l^oi8{ z#ER|LLR5SQUvk{O2zLGX5Lfzod(=q@IAL#KBO&I|b{OHYEkVUC+(xXKhTLH9Xgbk! zo@H&Ubc21pK^{eQ9&DOebig`n=>pIcLT;{D8xHg*A2}E9(4Ho1UY=KLIyilbG$jK?8ZM8T91^iA2Ab~&nI_{GuRM2z(q=XE zN`m)joD^P(y?wpd+xG0*F3g7mkV1HmTmwPZwO(@1hGSu>?wEhw3(15p!K2_)Awz~X z0^1jfi>JXd9{ObRpH2|_mkk_@|OF&8GaC-TQaVm@@<<-y1Dyw#hILcQ9Lk=PrK9<5-N_@WYPnNCjGM z@ioSDUb>(qQYw}ucg(ltq*KL(^?{8-+uH7_)&#E|dzA_ZwYb!1rIp^UCR(G1werPN zWZ~+bUEJ7g{kI!JnAUt~^HdHWrr(St7q}ZRp$20eGW44tAr3|XEvbuispeZdAN~Yx z;TCd4bjJ|Pd_t7Ajc8NQU0~OiQC+nFwF;!A9mhG7oK_lySOiapN_L6a=7xQ|xeM0G zRp|o30bK+NhlTM;8L?t3Q<;TsVX+rW7AKu;x7(mk`N9^`*6_@n#>nfV_Jy6xODPW2O9Crxj6x(?*Q%fo;B zd;pirg*Kc_`|OC7ctu&s){0fH^?1sX3#7#how*iqU6!)kPK zP~F{Pa5A93h0H+``?nK0+M2POxBZ`oI8r{{$^rxOSS4pGR`}hspwlmPUfPs7eh8Ee z_OI$@Q(o#D{~gIY3U-4c2stbKhA=eU^)$0~{omUrna9HGU{2-~gdglzFR31wrZH@+ zbffJ0n~F_4{2R_r32do)yerOZrZT*@8EMgkLhNqf{zq#KtH{9Kd^zdLx3sflV}+u3rzU7-%Bvx8>r}pg7@y+7 zZW<+k_(Q`>_pQ49eU5aT#>WwO4v)S)2iOYm9Ilm=0cOCo^_nhz?Os^#c2JwAA0=ER zz_s5(%X9w($?NKU_;n5~B%$rBM-`FqCHW!mF3X2yD6bcM)%>lJ-Odizi`#YdtU~j8 zzi!VM(k#rqS&+03=FYASIqK2h5^aP6%0KlJvNS`xHhqgq@Eo-rZ$u+G({cVWPRW;h z;i%dGL;?cGjU&>4*7NC&^&%#PjiaIlKFC(kyQ=zU-T8;C?_YctY*hr;b@*DdK4@74 z;cD+Bg%$7C)9QuX^TGRSp55B9o<$2EG}Ih|-k)iBc+Yjw9}=^&Ht>-6rJ(*f$-g}&pJ;1gqs>dMsi{tvM}_UKS5cSu!b4k3^GwbwhD+UgWOj*FE0o@MF@N*ct-a!(O}<2ixd5ER`>Niyc4< z&`{O3J+^5(tKLVr0AJBE82V>Ky&`S9`cTEp5KG}RZX~3KpDH7y$CxMEAYYeS+WZq= zchLyGZcy#!J@~qR40^O>fZ_lVmFged1W$Sw{H}72)BpIzVm|Q&#I{h@Y|+ofkYokK z>2HH%0=@We`~NSC$S>a@;}r!8GH~EJ15$XtZO02n5|J_*vcdyK6xQjXyVFHe5=cpE z`(woj4rhTT<){pCfo0iO67nu;Ri4(CDv#@`6v8V+J1e;w7#Y86Ef^zch` z4**{&kvV)=UX8KT2#%c3fX{M|nneDVBXmA;I2O);Ho63Zv_WP;Cy&4=?^6atijprN zRHBxjs);>hqt=d)0Jvoe7^S9Sw?)01PiX$Fh@>*2D z!N_wA@gy7+ z{#rXhBj-#9%mR(Qpio79mr_eNR*0|-ti41{BHLRf9)OL(jhT|CQSzYF8EwOJ&-}y7 z{~xL#N#HQ|KSrD<-+4daUOfTh!LF}!{~-KHnvUF3Dedr}7|w^cZg@eTjc7y=PFP0O zK1>B7$AT*j1XalqD(K^)_|zG&{9bD_*pdQ6Q<&wZ$&J3e*Dt8Nd_kmm?h7dj{?1MT zG?aGD>avwUTGF$Pwi~EA3&IeLSNQ2d=yvK8a`m&!Z~6293r%XZ2(SZo3nYtbc$zQP ziz;@jRa{XMy|!wTDw$AUG*-+Op>AOWQuC~Q1b^iW8hQcWc~GP3qpCsI&IJSU53cFx zA6|uA;;g4SkM)6V**X5WE>VhQWT7{dX8KIgnWl){CSx4e3gvK-hL1v9@+nOmy^>obR?1;>uXF63ZxY{6JORLkbg1cYu! zRPEd+W$B}l9ms&yUdk~p^~~wA({`5ib%7oK!dK5;(b#?F?3>4ij6NQqy&*kjTPR-p zS}Rc8GY#@8&DWsG&Uw|D-3F>2 z4m#y7fl%zmA+0`vm+cTH{5g;GPTq$teH}y!ai9mEfy2*lj%XgpM#vqM{tjY@eDwy^ zskBzUs%`VtW+DufCnirIuoaXea6BaL41P>W#%gOa4#x-O0I#Lq4_7?jCIMo6Y%p_d z7@UW(*a5~&rYe}<*lieu>!M6-vWuNy;xa%q(8f{-Dzz-u+pZu)Xr4Xkv&O&(=s#-G zZ-gF8c=NqD0}nMKRUP}&NQ^j@!T$>LpV=2r`i;=ym48FrM*?U=_^M~(M~hgJ3C`&A4AxA|S{75-VVH*Gx$!z%>0+Ijl}GwBUTraay> zPArxz78h|qBP!gibbY~}O;24j)f;EDr*-c$`uwRbckoMSs? zLVl|FR;+-wHli%RfQbb*7J`vA1$0f0HU#CrHtW&971qL?f+58(#r}>u|Fj=}`@&rf zg-3WTlzao;F@#CH`HRcy?kie`~o~RWG zX;1M2^**Jz=P8H^Hs;fuLAwGS;O>X*#$Jk)Bax?55^Hg0ni87`#vb$6+<^>6iUCWK zlae(F*c5(R41q|f_K1J7?2fobBQQF1DAeR`06w7E2!goTT zDJivB*9gccqEWJlv&oyttnCNQ)n`m#!97OaboDMP?Xtx$YT|VTOKo>Q;cBX&820*> zh|iZj?+7M1b%wj?qQ!-lszV5_w1|~0iV>??1 z9y3{O)iu8Lstd4k4n9|qaFis^jZHtbwp6VgB;9Txsbl0a)R9^OnOPd=#Z)(Q$b<8j z7xY=^frMn%cCD~cGiNIXqX2bGkF3wSb+CzA4LmGtG!=Y+OTB&Onyr^fQF+zs-_*!PTa=`#AoI`glDw zTLfrEeXV)mdVKU7f&JNlP#ZgrQ;;A9*lC7$0c%1!42G8pWGZ04v+8p=4ewr_TmhT} z!v0W&1R$cT*^9R5Rjmn9#11>aXQ-L~;={qjmWkYmjVFk;jMc65VRc|aUV76nVm;jHEF{^%OnlPqZC{rW@2Ml71hKSeBVc0?4$BUHrJ{wX z@9~UF^y>d>?^=VJJhO0!QUr!wr)U%ruc)X2R~J)B21u1%R79v@B#r=AV6S6 z!VX?oMJ?)B0$OWVyOBf?3_`>*qT`y=Hzv#Edoo9akyNjwyHz2kRFZC-$kh1$hT zMWb_90Ad4HS7~!?PRUpqat=$-9U6O7Sd-%-EV1!O<=&#FKOnLaMFpwS=;zW_wo&F>yAUTTQ!>$K@&NE$Uca zIaa8LjD%Q&3BA9(YC`|ly!~{!7NyxAS%^q-A>t3h^0T`ZiP_5T;@c_beyV>O1$_*{ zKdwK<42gE>wrN2vm`Wlq0DRn&kAr`u-o!VdYZuR1ul;Msle z1l!lp*Sni$>9h-;<6LV~C8W=Loy6+`uw^_8{gt;@S6`d)1 zF!l}(2kFb60Vl!OYO^Yp%dvoJO;QS$%}PLLGSQaoiM5abWkSL*iI)qG_9VSQW>XTU z^~A%EK&1OO6^B;d@mtb~a8HbAi;=7YrcYVm}9Zhf0`AUH^LSxlXYY;VIxaU|o{ zZ54O;z1_+-#M-+aVx?AJB{H8u%UOaBsW20=C=*?i;+TjKUDU5%^?tYM_#Gf;yXbo7 z2SPM)J$f80nSn62%s*c)2tZlw50zrp)``lmsN?NZ~4b`k> zre&`ada+4ke%G-HL7@kjc4t0uv|XXHu9Y;|s1g+Hc$w zx?A$RI*CTvg@jl{8&{B!8HdzfXwPbp!@g}6lAeBaXZN=JTg=anKCzdT0iSz@bt4Hv zA)b)zj;+Zmz4^OI&JOLH1pmn2YnmZ$Zds7>=%2)~F&MIrR0XEY)yuoBH>8jEA7jTC zTR(LXT{V47lx}!Oc=jQ@iJpaHmy-dR7|=Js9dtlSY(&0v0Xr8EB=5TA2hC*-MgsmGUhH%ZqH3{`lXS)A%ZBZ-BsLv8P(H>oxBGG3i9%RA97p@|6>u?adTE@ zrnvMO9LYk<`nu7=Pd%HVH^*T$hFT*WiQDJMv!PdyK%=_$bG7{VtIzSG5=%k?fM6za zEAqtqUv46`updb$cL5YmD@A5gCKAEMwk#jF-H>g&2SKUWQ@#<|(a@d_s&B4+$#R{$ zr1+;DwtvPf-te%jwaF;7LTj#7gG8S$! znKZ*cde(kyQ`^lKBnR|OK3~}mF_e9Z?$Y^)hAuKM^PrioxBFdq%gr4+L|p-p?z>K} z+;V0LeRQx>zm&q^xcr$%V=_O>6F^WG!K0&=WS%89+e?VF8VQmY)-G4&);2RJ4h6OCLr3f{N;>ms- z=2=at)!&Y8MHVr+k8+?}krD+ui{7e7oADgb}ZPKa_9ol~9L-uh<$&at_ zuOgZBF0ThPicPeg?_1-Gs)!gvS0CH%?Z3!}fx^b#vi|C2f7NV;K&CGG4uto)m{$-( zV%}o;YD4l_V+b>LII)bfIB-QcgQX=T$48<(sx>$+hZH**!J4c(Dkh15!j;0sv1$UJ zb=A2?41kY6fsdb)n9^1*ibp!$ky$j{O8xhewJa~nqgB}x;B3}B* z1`>yAXImr=Nn^@U>6fQTLvvqv_hkbBa+VDGbW9_4!WLx#2cI7w_RbIgduM|v6Jj%| zQeIkWX<+p@p@UsXqgfwY;qkHFH_~dP7y7IA6!d8@FzyqWVb}SSGB&O$nO`!>fGSlL zc#r(T&tb*-OgGGZ1;#rB6pS`B;!wYbHqK?LU5qouI8&&Ic(Z`~Kg<*o80E2|v7PSC zU2A=C__`b^UB{V$9_v00kf$#239GN~(E5I`yh9jKpy`{YYHnS7K*CSp2?IBb{eA=x`0Q*0=KrkDgat@uu}?$iqah z>o3|_(`e;?81v-er0E`)&R*0DEN)1|D4lWtR0K3=arxQ5C z#jg<}I)E@jLyJC+7M8 [!NOTE] +> This feature is only available in our sovity EDC Enterprise Edition. If you want to use the feature, please create a service request at sovity's service desk. + +## Overview + +OAuth2 protected APIs can be used for both Http-Data-Sources and Http-Data-Sinks. For both the +following properties can be used: + +| Property | Description | +|------------------------|--------------------------------------------------------------| +| oauth2:tokenUrl | Token-Url where the Access-Token can be fetched from | +| oauth2:clientId | The client id | +| oauth2:clientSecretKey | The vault key holding the client secret | + +> [!NOTE] +> The only supported flow right now is the "Client Credentials" flow. + +## Data Sources secured via OAuth2 + +### Providing the Asset via the UI + +To provide data from an OAuth2 protected API using the EDC-UI an asset with the +following `Custom Datasource Config (JSON)` can be created: + +```json +{ + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{target-url}}", + "oauth2:tokenUrl": "{{token-url}}", + "oauth2:clientId": "{{client-id}}", + "oauth2:clientSecretKey": "{{client-secret-key}}" +} +``` + +### Providing the Asset via the Management API + +To create an asset providing OAuth2 protected data the management-API of the EDC can be used to send the +following request: + +`POST` to `https://{{FQDN}}/api/management/v3/assets` + +> [!IMPORTANT] +> Be aware that while all other API examples work with API `v2` this example requires API `v3` + +```json +{ + "@type": "https://w3id.org/edc/v0.0.1/ns/Asset", + "https://w3id.org/edc/v0.0.1/ns/properties": { + "https://w3id.org/edc/v0.0.1/ns/id": "my-asset-1.0", + "http://www.w3.org/ns/dcat#version": "1.0", + "http://purl.org/dc/terms/language": "https://w3id.org/idsa/code/EN", + "http://purl.org/dc/terms/title": "test-document", + "http://purl.org/dc/terms/description": "my test document", + "http://www.w3.org/ns/dcat#keyword": [ + "keyword1", + "keyword2" + ], + "http://purl.org/dc/terms/creator": { + "http://xmlns.com/foaf/0.1/name": "My Org" + }, + "http://purl.org/dc/terms/license": "https://creativecommons.org/licenses/by/4.0/", + "http://www.w3.org/ns/dcat#landingPage": "https://mydepartment.myorg.com/my-offer", + "http://www.w3.org/ns/dcat#mediaType": "text/plain", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod": "false", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath": "false", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams": "false", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody": "false", + "http://purl.org/dc/terms/publisher": { + "http://xmlns.com/foaf/0.1/homepage": "https://myorg.com/" + } + }, + "https://w3id.org/edc/v0.0.1/ns/privateProperties": {}, + "https://w3id.org/edc/v0.0.1/ns/dataAddress": { + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{target-url}}", + "oauth2:tokenUrl": "{{token-url}}", + "oauth2:clientId": "{{client-id}}", + "oauth2:clientSecretKey": "{{client-secret-key}}" + } +} +``` + +## Data Sinks secured by OAuth2 + +### Initiating the Transfer via the UI + +To start a transfer to an OAuth2 protected API using the EDC-UI a transfer with the +following `Custom Datasink Config (JSON)` type can be started: + +```json +{ + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{target-url}}", + "oauth2:tokenUrl": "{{token-url}}", + "oauth2:clientId": "{{client-id}}", + "oauth2:clientSecretKey": "{{client-secret-key}}" +} +``` + +### Initiating the Transfer via the Management API + +To start a transfer to an OAuth2 protected API the management-API of the EDC can be used to send the +following request: + +`POST` to `https://{{FQDN}}/api/management/v2/transferprocesses` + +```json +{ + "@type": "https://w3id.org/edc/v0.0.1/ns/TransferRequest", + "https://w3id.org/edc/v0.0.1/ns/assetId": "{{ASSET_ID}}", + "https://w3id.org/edc/v0.0.1/ns/contractId": "{{CONTRACT_ID}}", + "https://w3id.org/edc/v0.0.1/ns/connectorAddress": "https://{{PROVIDER_EDC_FQDN}}/api/dsp", + "https://w3id.org/edc/v0.0.1/ns/connectorId": "{{PROVIDER_EDC_PARTICIPANT_ID}}", + "https://w3id.org/edc/v0.0.1/ns/dataDestination": { + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{target-url}}", + "oauth2:tokenUrl": "{{token-url}}", + "oauth2:clientId": "{{client-id}}", + "oauth2:clientSecretKey": "{{client-secret-key}}" + }, + "https://w3id.org/edc/v0.0.1/ns/properties": {}, + "https://w3id.org/edc/v0.0.1/ns/privateProperties": {}, + "https://w3id.org/edc/v0.0.1/ns/protocol": "dataspace-protocol-http", + "https://w3id.org/edc/v0.0.1/ns/managedResources": false +} +``` diff --git a/docs/getting-started/documentation/parameterized_assets.md b/docs/getting-started/documentation/parameterized_assets.md new file mode 100644 index 000000000..a9689eb2a --- /dev/null +++ b/docs/getting-started/documentation/parameterized_assets.md @@ -0,0 +1,51 @@ +How to share parameterized HTTP data sources to expose entire APIs +======== + +Provider: Create Asset (EDC UI) +======== +Create a `Custom Datasource Config (JSON)` asset over the edc-ui using the following Json: +```json +{ + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{DATA_SOURCE_URL}}", + "https://w3id.org/edc/v0.0.1/ns/proxyPath": "true", + "https://w3id.org/edc/v0.0.1/ns/proxyBody": "true", + "https://w3id.org/edc/v0.0.1/ns/proxyMethod": "true", + "https://w3id.org/edc/v0.0.1/ns/proxyQueryParams": "true" +} +``` + +Note: Proxy-Parameters are optional, you can use any combination of them. If you don't want to use a speceific Proxy-Parameter you can simply remove the whole line. + +Consumer: Start Transfer (Management-API) +======== +Start a transfer using the Management API using the following JSON: + +The relevant fields for API parametrization are located in the dataDestination section of the transfer process. + +Disclaimer: This is only working in the sovity EDC in combination with sovity EDC variants. + +Note: When using the body-parameterization a mediaType must also be specified. + +`POST` to `https://{{FQDN}}/api/management/v2/transferprocesses` +```json +{ + "@type": "https://w3id.org/edc/v0.0.1/ns/TransferRequest", + "https://w3id.org/edc/v0.0.1/ns/assetId": "{{ASSET_ID}}", + "https://w3id.org/edc/v0.0.1/ns/contractId": "{{CONTRACT_ID}}", + "https://w3id.org/edc/v0.0.1/ns/connectorAddress": "https://{{PROVIDER_EDC_FQDN}}/api/dsp", + "https://w3id.org/edc/v0.0.1/ns/connectorId": "{{PROVIDER_EDC_PARTICIPANT_ID}}", + "https://w3id.org/edc/v0.0.1/ns/dataDestination": { + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{DATA_SINK_URL}}", + "https://sovity.de/workaround/proxy/param/pathSegments": "{{PARAMETERIZATION_PATH}}", + "https://sovity.de/workaround/proxy/param/method": "{{PARAMETERIZATION_METHOD}}", + "https://sovity.de/workaround/proxy/param/queryParams": "{{PARAMETERIZATION_QUERY}}", + "https://sovity.de/workaround/proxy/param/mediaType": "{{PARAMETERIZATION_CONTENTTYPE}}", + "https://sovity.de/workaround/proxy/param/body": "{{PARAMETERIZATION_BODY}}" + }, + "https://w3id.org/edc/v0.0.1/ns/privateProperties": {}, + "https://w3id.org/edc/v0.0.1/ns/protocol": "dataspace-protocol-http", + "https://w3id.org/edc/v0.0.1/ns/managedResources": false +} +``` diff --git a/docs/getting-started/documentation/parameterized_assets_via_ui.md b/docs/getting-started/documentation/parameterized_assets_via_ui.md new file mode 100644 index 000000000..6ee077820 --- /dev/null +++ b/docs/getting-started/documentation/parameterized_assets_via_ui.md @@ -0,0 +1,56 @@ +How to share parameterized HTTP data sources to expose entire APIs using our UI +======== + +This guide will help you understand how to provide and consume parameterized HTTP data offers. + +## What are Parameterized Assets? + +Assets with Parameterized HTTP Data Sources are asset where you can change parts of the HTTP Request, e.g. choose a +different method, change the path, add query params or provide a custom request body. + +## Settings You Can Change +![parameterized-asset.png](screenshots/parameterized-asset.png) + +When you're choosing a HTTP Data asset in the user interface (UI), there are options you can turn on or off to let you +change certain parts of the HTTP request. These are called "overridable" fields because you can change, or "override," +the default settings. Here's what each option does: + +### Method Overridability: +When you're using an asset, and you've enabled "Method Overridability," you're required to specify a "Custom HTTP Method." + +A "method" is a type of request you make to a server. Common examples include GET (asking a server to show you a webpage), POST (sending information to a server, like filling out a form), PUT (updating information on a server), and DELETE (asking a server to remove information). So, when "Method Overridability" is enabled, you need to tell the system which of these types of requests to make to the server. This specified request type is your "Custom HTTP Method." + +### Path Overridability: +When "Path Overridability" is turned on, you have the ability to append additional paths to the URL specified by the provider. The path is the part of the URL that comes after the domain name. However, keep in mind that you're not replacing the entire path provided by the URL, but just adding to it at the end. + +### Query Parameter Overridability: +When "Query Parameter Overridability" is enabled, you can add or modify parameters in your HTTP request's URL. This is particularly useful when you need to customize your request. Simply put, the system will merge the default parameters of the HTTP Data asset and the ones you provide. In case of any conflicts, your parameters will have the upper hand. For example, if the default parameter is 'color=blue' and you provide 'color=red', your 'color' preference 'red' will be used. + +### Request Body Overridability: +When "Request Body Overridability" is enabled, you can alter the data you're sending to the server in the request body. This comes in handy when you're using methods like POST or PUT to send new data or update existing ones. If this feature is turned on, you can replace the default request body data, that comes with the HTTP Data asset, with your own. However, if it's not turned on, the system will use the default request body and you won't be able to modify it. + +#### Rules to Follow + +- `GET` **can't** have a request body. +- `POST`, `PUT`, `PATCH`, (and WebDAV's `PROPPATCH`, `REPORT`), **must** have a request body. + - When sending a body, the *Custom request body content type* **must** also be set, otherwise nothing will be sent. +- The other methods may or may not have a body. +- If you break these rules or forget to choose a method, the process will finish, but no data will be sent. +- `HEAD` is not supported. + +# Understanding Parameter Validation and Its Limitations + +When you're using our HTTP Data asset with parameterization enabled, it's important to understand how parameter validation works and the limitations you might encounter. + +## Invalid Parameters + +The HTTP Data asset validates parameters when making a request. If parameters are invalid, a notable issue is that the system reports a complete data transfer even when no data has been exchanged. For example, a GET request can never have a request body, and a PUT / PATCH request requires a request body. This happens because the validation checks parameters at the data plane stage, not at the control plane stage. + +## Missing Method + +If you don't provide a method for your request, even though you've turned on method overridability, your request can't be completed correctly. The system might not catch this issue because of the validation limitations mentioned above. + +## Asset Properties + +The parameterization process adds metadata to the asset that lets you know what kind of parameterization is enabled for the HTTP Data asset. However, due to an issue with asset metadata not getting persisted for consuming contract agreements, the available / required options aren't shown when initiating a transfer. Thus, currently, this information needs to be noted down manually from the catalog. + diff --git a/docs/getting-started/documentation/pull-data-transfer.md b/docs/getting-started/documentation/pull-data-transfer.md new file mode 100644 index 000000000..8268e4bf8 --- /dev/null +++ b/docs/getting-started/documentation/pull-data-transfer.md @@ -0,0 +1,97 @@ +Consuming Data via HttpProxy / HTTP Pull +======== + +> [!WARNING] +> This feature is only available for our sovity EDC Enterprise Edition. + +## Data-Transfer Architecture + +The following documentation describes the various supported data transfer architectures. +[Data Transfer Methods](./data-transfer-methods.md) + +## Requirements + +- An active contract agreement for a data offer you want to consume. +- A Use Case Application / Pull Backend that can be reached from the EDC, and that can reach the Data Planes of that + EDC. + +## Initiating the Transfer + +For the EDC send an EDR to your backend application, you need to initiate a transfer process. + +This "transfer process" represents the lifetime of your EDR in which your backend application can initiate as many +transfers as it wants, using the EDR it has received. + +### Initiating the Transfer via the UI + +When initiating the transfer, select `Custom Transfer Process Request (JSON)`, and provide: + +```json +{ + "@type": "https://w3id.org/edc/v0.0.1/ns/TransferRequest", + "https://w3id.org/edc/v0.0.1/ns/dataDestination": { + "https://w3id.org/edc/v0.0.1/ns/type": "HttpProxy" + }, + "https://w3id.org/edc/v0.0.1/ns/privateProperties": { + "https://w3id.org/edc/v0.0.1/ns/receiverHttpEndpoint": "{{TARGET_PULL_BACKEND_URL}}" + }, + "https://w3id.org/edc/v0.0.1/ns/protocol": "dataspace-protocol-http", + "https://w3id.org/edc/v0.0.1/ns/managedResources": false +} +``` + +### Initiating the Transfer via the Management API + +`POST` to `https://{{FQDN}}/api/management/v2/transferprocesses` + +```json +{ + "@type": "https://w3id.org/edc/v0.0.1/ns/TransferRequest", + "https://w3id.org/edc/v0.0.1/ns/assetId": "{{ASSET_ID}}", + "https://w3id.org/edc/v0.0.1/ns/contractId": "{{CONTRACT_ID}}", + "https://w3id.org/edc/v0.0.1/ns/connectorAddress": "https://{{PROVIDER_EDC_FQDN}}/api/dsp", + "https://w3id.org/edc/v0.0.1/ns/connectorId": "{{PROVIDER_EDC_PARTICIPANT_ID}}", + "https://w3id.org/edc/v0.0.1/ns/dataDestination": { + "https://w3id.org/edc/v0.0.1/ns/type": "HttpProxy", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "{{target-url}}" + }, + "https://w3id.org/edc/v0.0.1/ns/privateProperties": { + "https://w3id.org/edc/v0.0.1/ns/receiverHttpEndpoint": "{{target-pull-backend-url}}" + }, + "https://w3id.org/edc/v0.0.1/ns/protocol": "dataspace-protocol-http", + "https://w3id.org/edc/v0.0.1/ns/managedResources": false +} +``` + +## Receiving an Endpoint Data Reference (EDR) + +Your backend receives the EDR from the EDC by the EDC calling the `{{target-pull-backend-url}}` endpoint via `POST` method: +```json +{ + "id": "2d5348ea-b1e0-4b69-a625-07e7b093944a", + "endpoint": "http://connector-a-dataplane-1:8185/public", + "authKey": "Authorization", + "authCode": "Token ..." +} +``` + +## Getting the Data + +Using that EDR, requesting `GET` on the EDR's `{{endpoint}}` using the header `{{authKey}}: {{authCode}}` +will return the data. + +### Accessing the Contract ID + +The `authCode` JWT Token can be decoded to find the Contract Agreement ID. + +### Parameterized HTTP Data Sources + +- When method proxying is enabled on the providing side, the request method can be adjusted and will be used by the + providing EDC when fetching data from the data source. +- When path proxying is enabled on the providing side, any appended path to the `{{ endpoint }}` will be proxied through + to the data source. +- When query params proxying is enabled on the providing side, added query params will be passed through to the data + source. +- When request body proxying is enabled on the providing side, the request body and content-type headers will be proxied + to the provider side. + diff --git a/docs/getting-started/documentation/screenshots/parameterized-asset.png b/docs/getting-started/documentation/screenshots/parameterized-asset.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc4849e80de31e117c9953e8c3b78872abc9c1b GIT binary patch literal 26510 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 literal 0 HcmV?d00001 diff --git a/docs/postman_collection.json b/docs/postman_collection.json new file mode 100644 index 000000000..b9dd8929c --- /dev/null +++ b/docs/postman_collection.json @@ -0,0 +1,904 @@ +{ + "info": { + "_postman_id": "c9b798b5-5495-49a2-830a-9f8718f34266", + "name": "sovity EDC (0.2.1)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Assets", + "item": [ + { + "name": "1 Create Asset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"{{ASSET_ID}}\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\", \n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n }\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{PROVIDER_EDC_SOURCE_URL}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "1 Create Asset MDS", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"{{ASSET_ID}}\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n },\n \"https://w3id.org/mobilitydcat-ap/transport-mode\": \"Road\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category\": \"Traffic Information\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category\": \"Hazard Warnings\",\n \"https://w3id.org/mobilitydcat-ap/mobility-data-standard\": \"CSV\",\n \"https://w3id.org/mobilitydcat-ap/georeferencing-method\": \"Geo Ref Method Test\"\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{PROVIDER_EDC_SOURCE_URL}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "1 Delete Asset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/assets/{{ASSET_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "assets", + "{{ASSET_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "1 Request Assets", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/assets/request", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "assets", + "request" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Policies", + "item": [ + { + "name": "2 Create Simple Policy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/PolicyDefinition\",\n \"@id\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": []\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions" + ] + } + }, + "response": [] + }, + { + "name": "2 Create Time Policy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"PolicyDefinitionDto\",\n \"@id\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": [\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"POLICY_EVALUATION_TIME\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/gteq\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"2022-05-31T22:00:00.000Z\"\n },\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"POLICY_EVALUATION_TIME\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/lt\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"2030-06-30T22:00:00.000Z\"\n }\n ]\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions" + ] + } + }, + "response": [] + }, + { + "name": "2 Create Participant Policy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"PolicyDefinitionDto\",\n \"@id\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": [\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"REFERRING_CONNECTOR\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/eq\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"{{CONSUMER_EDC_PARTICIPANT_ID}}\"\n }\n ]\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions" + ] + } + }, + "response": [] + }, + { + "name": "2 Delete Policy", + "request": { + "method": "DELETE", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/{{POLICY_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions", + "{{POLICY_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "2 Request Policies", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/request", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions", + "request" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ContractDefinitions", + "item": [ + { + "name": "3 Create ContractDefinition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@id\": \"{{CONTRACT_DEFINITION_ID}}\",\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractDefinition\",\n \"https://w3id.org/edc/v0.0.1/ns/accessPolicyId\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/contractPolicyId\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/assetsSelector\": [\n {\n \"@type\": \"CriterionDto\",\n \"https://w3id.org/edc/v0.0.1/ns/operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"https://w3id.org/edc/v0.0.1/ns/operator\": \"=\",\n \"https://w3id.org/edc/v0.0.1/ns/operandRight\": \"{{ASSET_ID}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions" + ] + } + }, + "response": [] + }, + { + "name": "3 Delete ContractDefinition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/{{CONTRACT_DEFINITION_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions", + "{{CONTRACT_DEFINITION_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "3 Request ContractDefinitions", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/request", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions", + "request" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Catalog", + "item": [ + { + "name": "4 Request Catalog", + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/CatalogRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/counterPartyAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/querySpec\": {\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/catalog/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "catalog", + "request" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Negotiations", + "item": [ + { + "name": "5 Start Negotiation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/consumerId\": \"{{CONSUMER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/providerId\": \"{{PROVIDER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offer\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/offerId\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:ZjM4ZTJlMTItN2RmMC00ZjU3LTgwNDMtYjM0MzMwYTVkMDA3\",\r\n \"https://w3id.org/edc/v0.0.1/ns/assetId\": \"{{ASSET_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\r\n \"@id\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:ZjM4ZTJlMTItN2RmMC00ZjU3LTgwNDMtYjM0MzMwYTVkMDA3\",\r\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\r\n \"http://www.w3.org/ns/odrl/2/permission\": {\r\n \"http://www.w3.org/ns/odrl/2/target\": \"{{ASSET_ID}}\",\r\n \"http://www.w3.org/ns/odrl/2/action\": {\r\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\r\n },\r\n \"http://www.w3.org/ns/odrl/2/constraint\": []\r\n },\r\n \"http://www.w3.org/ns/odrl/2/prohibition\": [],\r\n \"http://www.w3.org/ns/odrl/2/obligation\": [],\r\n \"http://www.w3.org/ns/odrl/2/target\": \"{{ASSET_ID}}\"\r\n }\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations" + ] + } + }, + "response": [] + }, + { + "name": "5 Request Contract Negotiations", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "request" + ], + "query": [ + { + "key": "", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "5 Cancel Negotiation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/3f009db0-775d-4dfc-a965-decdf5a76aea/cancel", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "3f009db0-775d-4dfc-a965-decdf5a76aea", + "cancel" + ] + } + }, + "response": [] + }, + { + "name": "5 Decline Negotiation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/88687cb0-1d97-40c5-86c2-ad744afed538/decline", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "88687cb0-1d97-40c5-86c2-ad744afed538", + "decline" + ] + } + }, + "response": [] + }, + { + "name": "5 Get Negotiation", + "request": { + "method": "GET", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/88687cb0-1d97-40c5-86c2-ad744afed538", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "88687cb0-1d97-40c5-86c2-ad744afed538" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Agreements", + "item": [ + { + "name": "6 Request Contract Agreements", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractagreements/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractagreements", + "request" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Data Transfer", + "item": [ + { + "name": "7 Start Data Push", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TransferRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/assetId\": \"{{ASSET_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/contractId\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:MWZhMDk2YzEtODcwNi00NjBiLWJlMmYtZmQyNDFkZWQxYjE3\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorId\": \"{{PROVIDER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/dataDestination\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{CONSUMER_EDC_TRANSFER_TARGET_URL}}\"\r\n },\r\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {},\r\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/managedResources\": false\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses" + ] + } + }, + "response": [] + }, + { + "name": "8 Request Transfer Processes", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses", + "request" + ] + } + }, + "response": [] + }, + { + "name": "8 Cancel Transfer Process", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TerminateTransfer\",\r\n \"https://w3id.org/edc/v0.0.1/ns/reason\": \"Termination reason\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/c715355b-1e4b-49a9-9ef0-956405e88fe3/terminate", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses", + "c715355b-1e4b-49a9-9ef0-956405e88fe3", + "terminate" + ] + } + }, + "response": [] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "key", + "value": "X-Api-Key", + "type": "string" + }, + { + "key": "value", + "value": "ApiKeyDefaultValue", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "PROVIDER_EDC_MANAGEMENT_URL", + "value": "http://localhost:11002/api/management", + "type": "default" + }, + { + "key": "PROVIDER_EDC_PROTOCOL_URL", + "value": "http://edc:11003/api/dsp", + "type": "default" + }, + { + "key": "PROVIDER_EDC_PARTICIPANT_ID", + "value": "my-edc", + "type": "default" + }, + { + "key": "PROVIDER_EDC_SOURCE_URL", + "value": "https://api.github.com/repos/sovity/edc-extensions/events", + "type": "default" + }, + { + "key": "CONSUMER_EDC_MANAGEMENT_URL", + "value": "http://localhost:22002/api/management", + "type": "default" + }, + { + "key": "CONSUMER_EDC_PROTOCOL_URL", + "value": "http://edc2:11003/api/dsp", + "type": "default" + }, + { + "key": "CONSUMER_EDC_PARTICIPANT_ID", + "value": "my-edc2", + "type": "default" + }, + { + "key": "CONSUMER_EDC_TRANSFER_TARGET_URL", + "value": "https://webhook.site/a418c986-299d-4e22-a1e1-bf532631913a", + "type": "default" + }, + { + "key": "COUNTER", + "value": "1", + "type": "default" + }, + { + "key": "ASSET_ID", + "value": "http-source-{{COUNTER}}", + "type": "default" + }, + { + "key": "POLICY_ID", + "value": "policy-{{COUNTER}}", + "type": "default" + }, + { + "key": "CONTRACT_DEFINITION_ID", + "value": "contract-definition-{{COUNTER}}", + "type": "default" + } + ] +} diff --git a/docs/sovity-edc-api-wrapper.yaml b/docs/sovity-edc-api-wrapper.yaml new file mode 100644 index 000000000..2952736aa --- /dev/null +++ b/docs/sovity-edc-api-wrapper.yaml @@ -0,0 +1,1577 @@ +openapi: 3.0.1 +info: + title: sovity EDC API Wrapper + description: "sovity's EDC API Wrapper contains a selection of APIs for multiple\ + \ consumers, e.g. our EDC UI API, our generic Use Case API, our Commercial APIs,\ + \ etc. We bundled these APIs, so we can have an easier time generating our API\ + \ Client Libraries." + contact: + name: Sovity GmbH + url: https://github.com/sovity/edc-extensions/issues/new/choose + email: contact@sovity.de + license: + name: Apache 2.0 + url: https://github.com/sovity/edc-extensions/blob/main/LICENSE + version: 0.0.0 +externalDocs: + description: EDC API Wrapper Project in sovity/edc-extensions + url: https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper +servers: +- url: https://my-connector/api/management +tags: +- name: Enterprise Edition + description: sovity Enterprise Edition EDC API Endpoints. Requires our sovity Enterprise + Edition EDC Extensions. +- name: UI + description: EDC UI API Endpoints +- name: Use Case + description: Generic Use Case Application API Endpoints. +paths: + /wrapper/ee/connector-limits: + get: + tags: + - Enterprise Edition + description: Available and used resources of a connector. + operationId: connectorLimits + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectorLimits' + /wrapper/ee/file-upload/blobs/{blobId}/asset: + post: + tags: + - Enterprise Edition + summary: Create an asset from an uploaded file. + description: Creates an asset using the uploaded file as data source. + operationId: fileUploadCreateAsset + parameters: + - name: blobId + in: path + description: The Blob ID / URL the file was uploaded into. + required: true + schema: + type: string + requestBody: + description: Metadata for the Asset. File-related metadata might be overridden. + content: + application/json: + schema: + $ref: '#/components/schemas/UiAssetCreateRequest' + required: true + responses: + default: + description: default response + content: + '*/*': {} + /wrapper/ee/file-upload/blobs: + post: + tags: + - Enterprise Edition + summary: Requests a Blob for file upload. + description: Requests a Blob URL with a SAS Token so that the UI can directly + upload the file to the Azure Blob Storage. Returns the Blob ID / Token. + operationId: fileUploadRequestSasToken + responses: + default: + description: default response + content: + application/json: + schema: + type: string + /wrapper/ui/pages/asset-page/assets: + post: + tags: + - UI + description: Create a new Asset + operationId: createAsset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UiAssetCreateRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/contract-definition-page/contract-definitions: + post: + tags: + - UI + description: Create a new Contract Definition + operationId: createContractDefinition + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContractDefinitionRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/policy-page/policy-definitions: + post: + tags: + - UI + description: Create a new Policy Definition + operationId: createPolicyDefinition + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyDefinitionCreateRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/asset-page/assets/{assetId}: + delete: + tags: + - UI + description: Delete an Asset + operationId: deleteAsset + parameters: + - name: assetId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/contract-definition-page/contract-definitions/{contractDefinitionId}: + delete: + tags: + - UI + description: Delete a Contract Definition + operationId: deleteContractDefinition + parameters: + - name: contractDefinitionId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/policy-page/policy-definitions/{policyId}: + delete: + tags: + - UI + description: Delete a Policy Definition + operationId: deletePolicyDefinition + parameters: + - name: policyId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/asset-page/assets/{assetId}/metadata: + put: + tags: + - UI + description: Updates an Asset's metadata + operationId: editAssetMetadata + parameters: + - name: assetId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UiAssetEditMetadataRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/asset-page: + get: + tags: + - UI + description: Collect all data for Asset Page + operationId: getAssetPage + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/AssetPage' + /wrapper/ui/pages/catalog-page/data-offers: + get: + tags: + - UI + description: Fetch a connector's data offers + operationId: getCatalogPageDataOffers + parameters: + - name: connectorEndpoint + in: query + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UiDataOffer' + /wrapper/ui/pages/contract-agreement-page: + get: + tags: + - UI + description: Collect all data for the Contract Agreement Page + operationId: getContractAgreementPage + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ContractAgreementPage' + /wrapper/ui/pages/contract-definition-page: + get: + tags: + - UI + description: Collect all data for Contract Definition Page + operationId: getContractDefinitionPage + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ContractDefinitionPage' + /wrapper/ui/pages/catalog-page/contract-negotiations/{contractNegotiationId}: + get: + tags: + - UI + description: Get Contract Negotiation Information + operationId: getContractNegotiation + parameters: + - name: contractNegotiationId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/UiContractNegotiation' + /wrapper/ui/pages/dashboard-page: + get: + tags: + - UI + description: Collect all data for the Dashboard Page + operationId: getDashboardPage + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardPage' + /wrapper/ui/pages/policy-page: + get: + tags: + - UI + description: Collect all data for Policy Definition Page + operationId: getPolicyDefinitionPage + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyDefinitionPage' + /wrapper/ui/pages/transfer-history-page: + get: + tags: + - UI + description: Collect all data for the Transfer History Page + operationId: getTransferHistoryPage + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/TransferHistoryPage' + /wrapper/ui/pages/transfer-history-page/transfer-processes/{transferProcessId}/asset: + get: + tags: + - UI + description: Queries a transfer process' asset + operationId: getTransferProcessAsset + parameters: + - name: transferProcessId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/UiAsset' + /wrapper/ui/pages/catalog-page/contract-negotiations: + post: + tags: + - UI + description: Initiate a new Contract Negotiation + operationId: initiateContractNegotiation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContractNegotiationRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/UiContractNegotiation' + /wrapper/ui/pages/contract-agreement-page/transfers/custom: + post: + tags: + - UI + description: "Initiate a Transfer Process via a custom Transfer Process JSON-LD.\ + \ Fields such as connectorId, assetId, providerConnectorId, providerConnectorAddress\ + \ will be set automatically." + operationId: initiateCustomTransfer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InitiateCustomTransferRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/contract-agreement-page/transfers: + post: + tags: + - UI + description: Initiate a Transfer Process + operationId: initiateTransfer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InitiateTransferRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + /wrapper/use-case-api/kpis: + get: + tags: + - Use Case + description: Basic KPIs about the running EDC Connector. + operationId: getKpis + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/KpiResult' + /wrapper/use-case-api/supported-policy-functions: + get: + tags: + - Use Case + description: "List available functions in policies, prohibitions and obligations." + operationId: getSupportedFunctions + responses: + default: + description: default response + content: + application/json: + schema: + type: array + items: + type: string +components: + schemas: + ConnectorLimits: + required: + - numActiveConsumingContractAgreements + type: object + properties: + numActiveConsumingContractAgreements: + type: integer + description: Current amount of active consuming contract agreements. + format: int32 + maxActiveConsumingContractAgreements: + type: integer + description: Maximum amount of active consuming contract agreements. A value + of 'null' or a negative value means that there are no limit. + format: int32 + description: Available and used resources of a connector. + UiAssetCreateRequest: + required: + - dataAddressProperties + - id + type: object + properties: + id: + type: string + description: Asset Id + title: + type: string + description: Asset Title + language: + type: string + description: Asset Language + description: + type: string + description: Asset Description + publisherHomepage: + type: string + description: Asset Homepage + licenseUrl: + type: string + description: License URL + version: + type: string + description: Version + keywords: + type: array + description: Asset Keywords + items: + type: string + description: Asset Keywords + mediaType: + type: string + description: Asset MediaType + landingPageUrl: + type: string + description: Landing Page URL + dataCategory: + type: string + description: Data Category + dataSubcategory: + type: string + description: Data Subcategory + dataModel: + type: string + description: Data Model + geoReferenceMethod: + type: string + description: Geo-Reference Method + transportMode: + type: string + description: Transport Mode + sovereignLegalName: + type: string + description: The sovereign is distinct from the publisher by being the legal + owner of the data. + geoLocation: + type: string + description: Geo location + nutsLocations: + type: array + description: Locations by NUTS standard which divides countries into administrative + divisions + items: + type: string + description: Locations by NUTS standard which divides countries into administrative + divisions + dataSampleUrls: + type: array + description: Data sample URLs + items: + type: string + description: Data sample URLs + referenceFileUrls: + type: array + description: Reference file/schema URLs + items: + type: string + description: Reference file/schema URLs + referenceFilesDescription: + type: string + description: Additional information on reference files/schemas + conditionsForUse: + type: string + description: Instructions for use that are not legally relevant e.g. information + on how to cite the dataset in papers + dataUpdateFrequency: + type: string + description: Data update frequency + temporalCoverageFrom: + type: string + description: Temporal coverage start date + format: date + temporalCoverageToInclusive: + type: string + description: Temporal coverage end date (inclusive) + format: date + dataAddressProperties: + type: object + additionalProperties: + type: string + description: Data Address + description: Data Address + customJsonAsString: + type: string + description: Contains serialized custom properties in the JSON format. + customJsonLdAsString: + type: string + description: "Contains serialized custom properties in the JSON LD format.\ + \ Contrary to the customJsonAsString field, this string must represent\ + \ a JSON LD object and will be affected by JSON LD compaction and expansion.\ + \ Due to a technical limitation, the properties can't be booleans." + privateCustomJsonAsString: + type: string + description: Same as customJsonAsString but the data will be stored in the + private properties. + privateCustomJsonLdAsString: + type: string + description: Same as customJsonLdAsString but the data will be stored in + the private properties. The same limitations apply. + description: Type-Safe OpenAPI generator friendly Asset Create DTO that supports + an opinionated subset of the original EDC Asset Entity. + IdResponseDto: + required: + - id + - lastUpdatedDate + type: object + properties: + id: + type: string + description: ID + lastUpdatedDate: + type: string + description: Change Date + format: date-time + description: Marks the operation as successful + ContractDefinitionRequest: + required: + - accessPolicyId + - assetSelector + - contractDefinitionId + - contractPolicyId + type: object + properties: + contractDefinitionId: + type: string + description: Contract Definition ID + contractPolicyId: + type: string + description: Contract Policy ID + accessPolicyId: + type: string + description: Access Policy ID + assetSelector: + type: array + description: List of Criteria for the contract + items: + $ref: '#/components/schemas/UiCriterion' + description: Data for creating a Contract Definition + UiCriterion: + required: + - operandLeft + - operandRight + - operator + type: object + properties: + operandLeft: + type: string + description: Left Operand + operator: + $ref: '#/components/schemas/UiCriterionOperator' + operandRight: + $ref: '#/components/schemas/UiCriterionLiteral' + description: Contract Definition Criterion as supported by the UI + UiCriterionLiteral: + type: object + properties: + type: + $ref: '#/components/schemas/UiCriterionLiteralType' + value: + type: string + description: Only for type VALUE. The single value representation. + valueList: + type: array + description: "Only for type VALUE_LIST. List of values, e.g. for the IN-Operator." + items: + type: string + description: "Only for type VALUE_LIST. List of values, e.g. for the IN-Operator." + description: Criterion Literal + UiCriterionLiteralType: + type: string + description: Value type of an asset selector criterion right expression value + enum: + - VALUE + - VALUE_LIST + UiCriterionOperator: + type: string + description: Operator for constraints + enum: + - EQ + - IN + - LIKE + OperatorDto: + type: string + description: Operator for policies + enum: + - EQ + - NEQ + - GT + - GEQ + - LT + - LEQ + - IN + - HAS_PART + - IS_A + - IS_ALL_OF + - IS_ANY_OF + - IS_NONE_OF + PolicyDefinitionCreateRequest: + required: + - policy + - policyDefinitionId + type: object + properties: + policyDefinitionId: + type: string + description: Policy Definition ID + policy: + $ref: '#/components/schemas/UiPolicyCreateRequest' + description: Data for creating a Policy Definition + UiPolicyConstraint: + required: + - left + - operator + - right + type: object + properties: + left: + type: string + description: Left side of the expression. + operator: + $ref: '#/components/schemas/OperatorDto' + right: + $ref: '#/components/schemas/UiPolicyLiteral' + description: ODRL AtomicConstraint as supported by our UI + UiPolicyCreateRequest: + type: object + properties: + constraints: + type: array + description: Conjunction of required expressions for the policy to evaluate + to TRUE. + items: + $ref: '#/components/schemas/UiPolicyConstraint' + description: Type-Safe OpenAPI generator friendly Policy Create DTO that supports + an opinionated subset of the original EDC Policy Entity. + UiPolicyLiteral: + required: + - type + type: object + properties: + type: + $ref: '#/components/schemas/UiPolicyLiteralType' + value: + type: string + description: Only for types STRING and JSON + valueList: + type: array + description: Only for type STRING_LIST + items: + type: string + description: Only for type STRING_LIST + description: "Sum type: A String, a list of Strings or a generic JSON value." + UiPolicyLiteralType: + type: string + description: Supported Types of values for the right hand side of an expression + enum: + - STRING + - STRING_LIST + - JSON + UiAssetEditMetadataRequest: + type: object + properties: + title: + type: string + description: Asset Title + language: + type: string + description: Asset Language + description: + type: string + description: Asset Description + publisherHomepage: + type: string + description: Asset Homepage + licenseUrl: + type: string + description: License URL + version: + type: string + description: Version + keywords: + type: array + description: Asset Keywords + items: + type: string + description: Asset Keywords + mediaType: + type: string + description: Asset MediaType + landingPageUrl: + type: string + description: Landing Page URL + dataCategory: + type: string + description: Data Category + dataSubcategory: + type: string + description: Data Subcategory + dataModel: + type: string + description: Data Model + geoReferenceMethod: + type: string + description: Geo-Reference Method + transportMode: + type: string + description: Transport Mode + sovereignLegalName: + type: string + description: The sovereign is distinct from the publisher by being the legal + owner of the data. + geoLocation: + type: string + description: Geo location + nutsLocations: + type: array + description: Locations by NUTS standard which divides countries into administrative + divisions + items: + type: string + description: Locations by NUTS standard which divides countries into administrative + divisions + dataSampleUrls: + type: array + description: Data sample URLs + items: + type: string + description: Data sample URLs + referenceFileUrls: + type: array + description: Reference file/schema URLs + items: + type: string + description: Reference file/schema URLs + referenceFilesDescription: + type: string + description: Additional information on reference files/schemas + conditionsForUse: + type: string + description: Instructions for use that are not legally relevant e.g. information + on how to cite the dataset in papers + dataUpdateFrequency: + type: string + description: Data update frequency + temporalCoverageFrom: + type: string + description: Temporal coverage start date + format: date + temporalCoverageToInclusive: + type: string + description: Temporal coverage end date (inclusive) + format: date + customJsonAsString: + type: string + description: Contains serialized custom properties in the JSON format. + customJsonLdAsString: + type: string + description: "Contains serialized custom properties in the JSON LD format.\ + \ Contrary to the customJsonAsString field, this string must represent\ + \ a JSON LD object and will be affected by JSON LD compaction and expansion.\ + \ Due to a technical limitation, the properties can't be booleans." + privateCustomJsonAsString: + type: string + description: Same as customJsonAsString but the data will be stored in the + private properties. + privateCustomJsonLdAsString: + type: string + description: Same as customJsonLdAsString but the data will be stored in + the private properties. The same limitations apply. + description: Data for editing an asset. + AssetPage: + required: + - assets + type: object + properties: + assets: + type: array + description: Visible Assets + items: + $ref: '#/components/schemas/UiAsset' + description: All data for the Asset Page + UiAsset: + required: + - assetId + - connectorEndpoint + - creatorOrganizationName + - isOwnConnector + - participantId + - title + type: object + properties: + assetId: + type: string + description: Asset Id + connectorEndpoint: + type: string + description: Providing Connector's Connector Endpoint + participantId: + type: string + description: Providing Connector's Participant ID + title: + type: string + description: Asset Title + creatorOrganizationName: + type: string + description: Asset Organization Name + language: + type: string + description: Asset Language + description: + type: string + description: Asset Description. Supports markdown. + descriptionShortText: + type: string + description: Asset Description Short Text generated from description. Contains + no markdown. + isOwnConnector: + type: boolean + description: Flag that indicates whether this asset is created by this connector. + publisherHomepage: + type: string + description: Asset Homepage + licenseUrl: + type: string + description: License URL + version: + type: string + description: Version + keywords: + type: array + description: Asset Keywords + items: + type: string + description: Asset Keywords + mediaType: + type: string + description: Asset MediaType + landingPageUrl: + type: string + description: Homepage URL associated with the Asset + httpDatasourceHintsProxyMethod: + type: boolean + description: HTTP Datasource Hints Proxy Method + httpDatasourceHintsProxyPath: + type: boolean + description: HTTP Datasource Hints Proxy Path + httpDatasourceHintsProxyQueryParams: + type: boolean + description: HTTP Datasource Hints Proxy Query Params + httpDatasourceHintsProxyBody: + type: boolean + description: HTTP Datasource Hints Proxy Body + dataCategory: + type: string + description: Data Category + dataSubcategory: + type: string + description: Data Subcategory + dataModel: + type: string + description: Data Model + geoReferenceMethod: + type: string + description: Geo-Reference Method + transportMode: + type: string + description: Transport Mode + sovereignLegalName: + type: string + description: The sovereign is distinct from the publisher by being the legal + owner of the data. + geoLocation: + type: string + description: Geo location + nutsLocations: + type: array + description: Locations by NUTS standard which divides countries into administrative + divisions + items: + type: string + description: Locations by NUTS standard which divides countries into administrative + divisions + dataSampleUrls: + type: array + description: Data sample URLs + items: + type: string + description: Data sample URLs + referenceFileUrls: + type: array + description: Reference file/schema URLs + items: + type: string + description: Reference file/schema URLs + referenceFilesDescription: + type: string + description: Additional information on reference files/schemas + conditionsForUse: + type: string + description: Instructions for use that are not legally relevant e.g. information + on how to cite the dataset in papers + dataUpdateFrequency: + type: string + description: Data update frequency + temporalCoverageFrom: + type: string + description: Temporal coverage start date + format: date + temporalCoverageToInclusive: + type: string + description: Temporal coverage end date (inclusive) + format: date + assetJsonLd: + type: string + description: Contains the entire asset in the JSON-LD format + customJsonAsString: + type: string + description: Contains serialized custom properties in the JSON format. + customJsonLdAsString: + type: string + description: "Contains serialized custom properties in the JSON LD format.\ + \ Contrary to the customJsonAsString field, this string must represent\ + \ a JSON LD object and will be affected by JSON LD compaction and expansion.\ + \ Due to a technical limitation, the properties can't be booleans." + privateCustomJsonAsString: + type: string + description: Same as customJsonAsString but the data will be stored in the + private properties. + privateCustomJsonLdAsString: + type: string + description: Same as customJsonLdAsString but the data will be stored in + the private properties. The same limitations apply. + description: Type-Safe Asset Metadata as needed by our UI + UiContractOffer: + required: + - contractOfferId + - policy + type: object + properties: + contractOfferId: + type: string + description: Contract Offer ID + policy: + $ref: '#/components/schemas/UiPolicy' + description: Catalog Data Offer's Contract Offer as required by the UI + UiDataOffer: + required: + - asset + - contractOffers + - endpoint + - participantId + type: object + properties: + endpoint: + type: string + description: Connector Endpoint + participantId: + type: string + description: Participant ID. Required for initiating transfers. + asset: + $ref: '#/components/schemas/UiAsset' + contractOffers: + type: array + description: Available Contract Offers + items: + $ref: '#/components/schemas/UiContractOffer' + description: Catalog Data Offer as required by the UI + UiPolicy: + required: + - errors + - policyJsonLd + type: object + properties: + policyJsonLd: + type: string + description: EDC Policy JSON-LD. This is required because the EDC requires + the full policy when initiating contract negotiations. + constraints: + type: array + description: Conjunction of required expressions for the policy to evaluate + to TRUE. + items: + $ref: '#/components/schemas/UiPolicyConstraint' + errors: + type: array + description: "When trying to reduce the policy JSON-LD to our opinionated\ + \ subset of functionalities, many fields and functionalities are unsupported.\ + \ Should any discrepancies occur during the mapping process, we'll collect\ + \ them here." + items: + type: string + description: "When trying to reduce the policy JSON-LD to our opinionated\ + \ subset of functionalities, many fields and functionalities are unsupported.\ + \ Should any discrepancies occur during the mapping process, we'll collect\ + \ them here." + description: Type-Safe OpenAPI generator friendly Policy DTO as needed by our + UI + ContractAgreementCard: + required: + - asset + - contractAgreementId + - contractNegotiationId + - contractPolicy + - contractSigningDate + - counterPartyAddress + - counterPartyId + - direction + - transferProcesses + type: object + properties: + contractAgreementId: + type: string + description: Contract Agreement ID + contractNegotiationId: + type: string + description: Contract Negotiation ID + direction: + $ref: '#/components/schemas/ContractAgreementDirection' + counterPartyAddress: + type: string + description: Other Connector's Endpoint + counterPartyId: + type: string + description: Other Connector's ID + contractSigningDate: + type: string + description: Contract Agreements Signing Date + format: date-time + asset: + $ref: '#/components/schemas/UiAsset' + contractPolicy: + $ref: '#/components/schemas/UiPolicy' + transferProcesses: + type: array + description: Contract Agreement's Transfer Processes + items: + $ref: '#/components/schemas/ContractAgreementTransferProcess' + description: Contract Agreement for Contract Agreement Page + ContractAgreementDirection: + type: string + description: Whether the contract agreement is incoming or outgoing + enum: + - CONSUMING + - PROVIDING + ContractAgreementPage: + required: + - contractAgreements + type: object + properties: + contractAgreements: + type: array + description: Contract Agreement Cards + items: + $ref: '#/components/schemas/ContractAgreementCard' + ContractAgreementTransferProcess: + required: + - lastUpdatedDate + - state + - transferProcessId + type: object + properties: + transferProcessId: + type: string + description: Transfer Process ID + lastUpdatedDate: + type: string + description: Last Change Date + format: date-time + state: + $ref: '#/components/schemas/TransferProcessState' + errorMessage: + type: string + description: Error Message + description: A Contract Agreement's Transfer Process + TransferProcessSimplifiedState: + type: string + description: Simplified Transfer Process State to be used in UI + enum: + - RUNNING + - OK + - ERROR + TransferProcessState: + required: + - code + - name + - simplifiedState + type: object + properties: + name: + type: string + description: State name or 'CUSTOM'. State names only exist for original + EDC Transfer Process States. + code: + type: integer + description: State code + format: int32 + simplifiedState: + $ref: '#/components/schemas/TransferProcessSimplifiedState' + description: Transfer Process State interpreted + ContractDefinitionEntry: + required: + - accessPolicyId + - assetSelector + - contractDefinitionId + - contractPolicyId + type: object + properties: + contractDefinitionId: + type: string + description: Contract Definition ID + accessPolicyId: + type: string + description: Access Policy ID + contractPolicyId: + type: string + description: Contract Policy ID + assetSelector: + type: array + description: Criteria for the contract + items: + $ref: '#/components/schemas/UiCriterion' + description: Contract Definition List Entry for Contract Definition Page + ContractDefinitionPage: + required: + - contractDefinitions + type: object + properties: + contractDefinitions: + type: array + description: Contract Definition Entries + items: + $ref: '#/components/schemas/ContractDefinitionEntry' + description: All data for the Contract Definition Page + ContractNegotiationSimplifiedState: + type: string + description: Simplified Contract Negotiation State to be used in UI + enum: + - IN_PROGRESS + - AGREED + - TERMINATED + ContractNegotiationState: + required: + - code + - name + - simplifiedState + type: object + properties: + name: + type: string + description: State name or 'CUSTOM'. State names only exist for original + EDC Contract Negotiation States. + code: + type: integer + description: State code + format: int32 + simplifiedState: + $ref: '#/components/schemas/ContractNegotiationSimplifiedState' + description: Contract Negotiation State interpreted + UiContractNegotiation: + required: + - contractNegotiationId + - createdAt + - state + type: object + properties: + contractNegotiationId: + type: string + description: Contract Negotiation Id + createdAt: + type: string + description: Contract Negotiation Creation Time + format: date-time + contractAgreementId: + type: string + description: Contract Agreement Id + state: + $ref: '#/components/schemas/ContractNegotiationState' + description: Contract Negotiation Information + DashboardDapsConfig: + required: + - jwksUrl + - tokenUrl + type: object + properties: + tokenUrl: + type: string + description: Your Connector's DAPS Token URL + jwksUrl: + type: string + description: Your Connector's DAPS JWKS URL + description: DAPS Config + DashboardMiwConfig: + required: + - authorityId + - tokenUrl + - url + type: object + properties: + url: + type: string + description: Your Connector's MIW's URL + tokenUrl: + type: string + description: Your Connector's MIW's Token URL + authorityId: + type: string + description: Your Connector's MIW's Authority ID + description: Managed Identity Wallet (MIW) Config + DashboardPage: + required: + - connectorCuratorName + - connectorCuratorUrl + - connectorDescription + - connectorEndpoint + - connectorMaintainerName + - connectorMaintainerUrl + - connectorParticipantId + - connectorTitle + - numAssets + - numContractAgreementsConsuming + - numContractAgreementsProviding + - numContractDefinitions + - numPolicies + - transferProcessesConsuming + - transferProcessesProviding + type: object + properties: + numAssets: + type: integer + description: Number of Assets + format: int32 + numPolicies: + type: integer + description: Number of Policies + format: int32 + numContractDefinitions: + type: integer + description: Number of Contract Definitions + format: int32 + numContractAgreementsConsuming: + type: integer + description: Number of consuming Contract Agreements + format: int64 + numContractAgreementsProviding: + type: integer + description: Number of providing Contract Agreements + format: int64 + transferProcessesConsuming: + $ref: '#/components/schemas/DashboardTransferAmounts' + transferProcessesProviding: + $ref: '#/components/schemas/DashboardTransferAmounts' + connectorEndpoint: + type: string + description: Your Connector's Connector Endpoint + connectorParticipantId: + type: string + description: Your Connector's Participant ID + connectorTitle: + type: string + description: Your Connector's Title + connectorDescription: + type: string + description: Your Connector's Description + connectorCuratorUrl: + type: string + description: Your Organization Homepage + connectorCuratorName: + type: string + description: Your Organization Name + connectorMaintainerUrl: + type: string + description: Your Connector's Maintainer's Organization Homepage + connectorMaintainerName: + type: string + description: Your Connector's Maintainer's Organization Name + connectorDapsConfig: + $ref: '#/components/schemas/DashboardDapsConfig' + connectorMiwConfig: + $ref: '#/components/schemas/DashboardMiwConfig' + DashboardTransferAmounts: + required: + - numError + - numOk + - numRunning + - numTotal + type: object + properties: + numTotal: + type: integer + description: Number of Transfer Processes + format: int64 + numRunning: + type: integer + description: Number of running Transfer Processes + format: int64 + numOk: + type: integer + description: Number of successful Transfer Processes + format: int64 + numError: + type: integer + description: Number of failed Transfer Processes + format: int64 + description: Providing Transfer Process Amounts + PolicyDefinitionDto: + required: + - policy + - policyDefinitionId + type: object + properties: + policyDefinitionId: + type: string + description: Policy Definition ID + policy: + $ref: '#/components/schemas/UiPolicy' + description: Policy Definition as required for the Policy Definition Page + PolicyDefinitionPage: + required: + - policies + type: object + properties: + policies: + type: array + description: Policy Definition Entries + items: + $ref: '#/components/schemas/PolicyDefinitionDto' + description: All data for the policy definition page as required by the UI + TransferHistoryEntry: + required: + - assetId + - assetName + - contractAgreementId + - counterPartyConnectorEndpoint + - counterPartyParticipantId + - createdDate + - direction + - lastUpdatedDate + - state + - transferProcessId + type: object + properties: + transferProcessId: + type: string + description: Transfer Process ID + createdDate: + type: string + description: Created Date + format: date-time + lastUpdatedDate: + type: string + description: Last Change Date + format: date-time + state: + $ref: '#/components/schemas/TransferProcessState' + contractAgreementId: + type: string + description: Contract Agreement ID + direction: + $ref: '#/components/schemas/ContractAgreementDirection' + counterPartyConnectorEndpoint: + type: string + description: Other Connector's Endpoint + counterPartyParticipantId: + type: string + description: Other Connector's Participant ID + assetName: + type: string + description: Asset Name + assetId: + type: string + description: Asset ID + errorMessage: + type: string + description: Error Message + description: Transfer History Entry for Transfer History Page + TransferHistoryPage: + required: + - transferEntries + type: object + properties: + transferEntries: + type: array + description: Transfer History Page Entries + items: + $ref: '#/components/schemas/TransferHistoryEntry' + ContractNegotiationRequest: + required: + - assetId + - contractOfferId + - counterPartyAddress + - counterPartyParticipantId + - policyJsonLd + type: object + properties: + counterPartyAddress: + type: string + description: Counter Party Address + counterPartyParticipantId: + type: string + description: Counter Party Participant ID + contractOfferId: + type: string + description: 'Contract Offer Dto ' + policyJsonLd: + type: string + description: Policy JsonLd + assetId: + type: string + description: Asset ID + description: Data for initiating a Contract Negotiation + InitiateCustomTransferRequest: + required: + - contractAgreementId + - transferProcessRequestJsonLd + type: object + properties: + contractAgreementId: + type: string + description: Contract Agreement ID + transferProcessRequestJsonLd: + type: string + description: "Partial TransferProcessRequestJsonLd JSON-LD. Fields participantId,\ + \ connectorEndpoint, assetId and contractId can be omitted, they will\ + \ be overridden with information from the contract." + description: Required data for starting a Contract Agreement's Transfer Process + InitiateTransferRequest: + required: + - contractAgreementId + - dataSinkProperties + - transferProcessProperties + type: object + properties: + contractAgreementId: + type: string + description: Contract Agreement ID + dataSinkProperties: + type: object + additionalProperties: + type: string + description: Data Sink / Data Address + description: Data Sink / Data Address + transferProcessProperties: + type: object + additionalProperties: + type: string + description: Additional transfer process properties. These are not passed + to the consumer EDC + description: Additional transfer process properties. These are not passed + to the consumer EDC + description: "For type PARAMS_ONLY: Required data for starting a Transfer Process" + KpiResult: + required: + - assetsCount + - contractAgreementsCount + - contractDefinitionsCount + - policiesCount + - transferProcessDto + type: object + properties: + assetsCount: + type: integer + description: Counts of assets + format: int32 + policiesCount: + type: integer + description: Counts of policies + format: int32 + contractDefinitionsCount: + type: integer + description: Counts of contract definitions + format: int32 + contractAgreementsCount: + type: integer + description: Counts of contract agreements + format: int32 + transferProcessDto: + $ref: '#/components/schemas/TransferProcessStatesDto' + description: EDC-status-defining KPIs + TransferProcessStatesDto: + required: + - incomingTransferProcessCounts + - outgoingTransferProcessCounts + type: object + properties: + incomingTransferProcessCounts: + type: object + additionalProperties: + type: integer + description: States and count of incoming transferprocess counts + format: int64 + description: States and count of incoming transferprocess counts + outgoingTransferProcessCounts: + type: object + additionalProperties: + type: integer + description: States and counts of outgoing transferprocess counts + format: int64 + description: States and counts of outgoing transferprocess counts + description: Counts of incoming and outgoing TransferProcesses and status diff --git a/extensions/edc-ui-config/README.md b/extensions/edc-ui-config/README.md new file mode 100644 index 000000000..6acd43b70 --- /dev/null +++ b/extensions/edc-ui-config/README.md @@ -0,0 +1,36 @@ + +
      +

      + + Logo + + +

      EDC-Connector Extension:
      EDC UI Extension Config

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension + +Our [EDC UI](https://github.com/sovity/edc-ui/) requires many configuration properties which exist in the EDC Backend. + +This extension provides an endpoint on the Management Endpoint `/edc-ui-config` which allows our EDC UI to retrieve +additional `EDC_UI_` properties from the backend. + +It will pass all config properties starting with `edc.ui.` in general. + +## Why does this extension exist? + +By not having to repeat ourselves when configuring the EDC UI, we save time and reduce the risk of errors. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/edc-ui-config/build.gradle.kts b/extensions/edc-ui-config/build.gradle.kts new file mode 100644 index 000000000..a5d78cdaf --- /dev/null +++ b/extensions/edc-ui-config/build.gradle.kts @@ -0,0 +1,57 @@ +val edcVersion: String by project +val edcGroup: String by project +val restAssured: String by project +val jettyVersion: String by project +val jettyGroup: String by project +val mockitoVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:control-plane-spi:${edcVersion}") + implementation("${edcGroup}:api-core:${edcVersion}") + implementation("${edcGroup}:management-api-configuration:${edcVersion}") + + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + + testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation("${edcGroup}:http:${edcVersion}") { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation("${jettyGroup}:jetty-client:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-http:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-io:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-server:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-util:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-webapp:${jettyVersion}") + + testImplementation("io.rest-assured:rest-assured:${restAssured}") + testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigController.java b/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigController.java new file mode 100644 index 000000000..a9a4fc4ea --- /dev/null +++ b/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigController.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import java.util.Map; + +@Produces({MediaType.APPLICATION_JSON}) +@Path("/edc-ui-config") +public class EdcUiConfigController { + private final EdcUiConfigService edcUiConfigService; + + public EdcUiConfigController(EdcUiConfigService edcUiConfigService) { + this.edcUiConfigService = edcUiConfigService; + } + + @GET + public Map getEdcUiProperties() { + return edcUiConfigService.getEdcUiProperties(); + } +} diff --git a/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigExtension.java b/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigExtension.java new file mode 100644 index 000000000..fd8461366 --- /dev/null +++ b/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigExtension.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +public class EdcUiConfigExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "EdcUiConfigExtension"; + + @Inject + private ManagementApiConfiguration config; + + @Inject + private WebService webService; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var edcUiConfigService = new EdcUiConfigService(context.getConfig()); + var controller = new EdcUiConfigController(edcUiConfigService); + webService.registerResource(config.getContextAlias(), controller); + } +} diff --git a/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigService.java b/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigService.java new file mode 100644 index 000000000..df855c2c1 --- /dev/null +++ b/extensions/edc-ui-config/src/main/java/de/sovity/edc/extension/EdcUiConfigService.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import org.eclipse.edc.spi.system.configuration.Config; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class EdcUiConfigService { + + private final Config config; + + public EdcUiConfigService(Config config) { + this.config = config; + } + + public Map getEdcUiProperties() { + return mapKeys(config.getRelativeEntries("edc.ui."), this::toFullCapsSnakeCase); + } + + private String toFullCapsSnakeCase(String edcPropertyName) { + return edcPropertyName.replace(".", "_").toUpperCase(); + } + + private Map mapKeys(Map map, Function mapper) { + return map.keySet().stream().collect(Collectors.toMap(mapper, map::get)); + } +} diff --git a/extensions/edc-ui-config/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/edc-ui-config/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..a9fe7110c --- /dev/null +++ b/extensions/edc-ui-config/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.EdcUiConfigExtension diff --git a/extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/EdcUiConfigTest.java b/extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/EdcUiConfigTest.java new file mode 100644 index 000000000..e1771495a --- /dev/null +++ b/extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/EdcUiConfigTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.version.controller; + +import org.eclipse.edc.connector.dataplane.selector.spi.store.DataPlaneInstanceStore; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.protocol.ProtocolWebhook; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static de.sovity.edc.extension.version.controller.TestUtils.createConfiguration; +import static de.sovity.edc.extension.version.controller.TestUtils.mockRequest; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +@ApiTest +@ExtendWith(EdcExtension.class) +class EdcUiConfigTest { + private static final String SOME_EXAMPLE_PROP = "this should also be passed through"; + + @BeforeEach + void setUp(EdcExtension extension) { + extension.registerServiceMock(ProtocolWebhook.class, mock(ProtocolWebhook.class)); + extension.registerServiceMock(JsonLd.class, mock(JsonLd.class)); + extension.registerServiceMock( + DataPlaneInstanceStore.class, + mock(DataPlaneInstanceStore.class)); + extension.setConfiguration(createConfiguration(Map.of( + "edc.ui.some.example.prop", SOME_EXAMPLE_PROP + ))); + } + + @Test + void testEdcUiConfigWithEverythingSet() { + mockRequest().assertThat() + .body("EDC_UI_SOME_EXAMPLE_PROP", equalTo(SOME_EXAMPLE_PROP)); + } +} diff --git a/extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java b/extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java new file mode 100644 index 000000000..9efdb4775 --- /dev/null +++ b/extensions/edc-ui-config/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.version.controller; + +import io.restassured.http.ContentType; +import io.restassured.response.ValidatableResponse; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; + +public class TestUtils { + + private static final int DATA_PORT = getFreePort(); + private static final String API_KEY = "123456"; + + @NotNull + static Map createConfiguration(Map additionalProps) { + Map props = new HashMap<>(); + props.put("web.http.port", String.valueOf(getFreePort())); + props.put("web.http.path", "/api"); + props.put("web.http.management.port", String.valueOf(DATA_PORT)); + props.put("web.http.management.path", "/api/v1/data"); + props.put("edc.api.auth.key", API_KEY); + props.putAll(additionalProps); + return props; + } + + static ValidatableResponse mockRequest() { + return given() + .baseUri("http://localhost:" + DATA_PORT) + .basePath("/api/v1/data") + .header("X-Api-Key", API_KEY) + .when() + .contentType(ContentType.TEXT) + .get("/edc-ui-config") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} diff --git a/extensions/last-commit-info/README.md b/extensions/last-commit-info/README.md new file mode 100644 index 000000000..ea959cd9f --- /dev/null +++ b/extensions/last-commit-info/README.md @@ -0,0 +1,64 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Last Commit Info

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension +It adds an endpoint `/last-commit-info` on the Management API which returns the commit information of +the last commit of built EDC Connector. Example: +``` +commit eabb87eb8c76a82e022ff0400b4b529348d902f4 (HEAD -> main, origin/main) +Merge: 15ece734e 225b18251 +Author: First Last +Date: Mon Mar 13 08:09:15 2023 +0100 + + Merge pull request #221 from sovity/feat/ms8 + + chore: update to milestone-8 + +Jar Last Commit Info: +commit 2fe06beaf6027fb4cc06db2adb7d5b4c8ae61b05 (HEAD -> 2023-03-16-edc-extensions-cleanup, origin/2023-03-16-edc-extensions-cleanup) +Author: First Last +Date: Thu Mar 9 14:50:20 2023 +0100 + + feat new image variants, rework documentation, better configuration via MY_EDC vars +``` + +## Why does this extension exist? + +Building EDC Connectors requires one to build one's own EDC Connector and EDC Connector Images. + +We need a way to find out the version of running EDC Connector instances. + +We found that finding the last commit of the EDC Connector Image was the most accurate way we could judge the +running EDC Connector instance. + +Since our EDC Images use our EDC Extensions from this repository we also embed a second "jar last commit info" +during build time of the edc-extensions, which represent all other EDC Extensions of this repository, since +they will always be used with the same version. + +## Configuration + +### Connector Version +The ENV var `EDC_LAST_COMMIT_INFO` should be set in the Dockerfile via build args. + +### JAR Version +The contents of the file `src/main/resources/jar-last-commit-info.txt` will be adjusted by the +repository's GitHub pipeline to reflect the last commit info. + +## License +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact +sovity GmbH - contact@sovity.de diff --git a/extensions/last-commit-info/build.gradle.kts b/extensions/last-commit-info/build.gradle.kts new file mode 100644 index 000000000..80a8227cf --- /dev/null +++ b/extensions/last-commit-info/build.gradle.kts @@ -0,0 +1,64 @@ +val edcVersion: String by project +val edcGroup: String by project +val restAssured: String by project +val mockitoVersion: String by project +val lombokVersion: String by project +val jettyVersion: String by project +val jettyGroup: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:control-plane-spi:${edcVersion}") + implementation("${edcGroup}:api-core:${edcVersion}") + implementation("${edcGroup}:management-api-configuration:${edcVersion}") + + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + + testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation("${edcGroup}:http:${edcVersion}") { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation("${jettyGroup}:jetty-client:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-http:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-io:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-server:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-util:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-webapp:${jettyVersion}") + + testImplementation("io.rest-assured:rest-assured:${restAssured}") + testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfo.java b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfo.java new file mode 100644 index 000000000..99e52ea91 --- /dev/null +++ b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfo.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LastCommitInfo { + private String envLastCommitInfo; + private String envBuildDate; + private String jarLastCommitInfo; + private String jarBuildDate; +} diff --git a/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoController.java b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoController.java new file mode 100644 index 000000000..9f7994694 --- /dev/null +++ b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoController.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Produces({MediaType.APPLICATION_JSON}) +@Path("/last-commit-info") +public class LastCommitInfoController { + private final LastCommitInfoService lastCommitInfoService; + + public LastCommitInfoController(LastCommitInfoService lastCommitInfoService) { + this.lastCommitInfoService = lastCommitInfoService; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public LastCommitInfo getLastCommitInfo() { + return lastCommitInfoService.getLastCommitInfo(); + } +} diff --git a/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoExtension.java b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoExtension.java new file mode 100644 index 000000000..b976fe670 --- /dev/null +++ b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoExtension.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +public class LastCommitInfoExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "LastCommitInfoExtension"; + @Inject + private ManagementApiConfiguration config; + @Inject + private WebService webService; + + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var lastCommitInfoService = new LastCommitInfoService(context); + var controller = new LastCommitInfoController(lastCommitInfoService); + webService.registerResource(config.getContextAlias(), controller); + } +} diff --git a/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoService.java b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoService.java new file mode 100644 index 000000000..e39f9363b --- /dev/null +++ b/extensions/last-commit-info/src/main/java/de/sovity/edc/extension/LastCommitInfoService.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension; + +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Scanner; + +public class LastCommitInfoService { + + private final ServiceExtensionContext context; + + public LastCommitInfoService(ServiceExtensionContext context) { + this.context = context; + } + + private String readFileInCurrentClassClasspath(String path) { + var classLoader = LastCommitInfoService.class.getClassLoader(); + var is = classLoader.getResourceAsStream(path); + var scanner = new Scanner(Objects.requireNonNull(is), StandardCharsets.UTF_8).useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; + } + + + public String getJarLastCommitInfo() { + return this.readFileInCurrentClassClasspath("jar-last-commit-info.txt").trim(); + } + + public String getEnvLastCommitInfo() { + return context.getSetting("edc.last.commit.info", ""); + } + + public String getJarBuildDate() { + return readFileInCurrentClassClasspath("jar-build-date.txt").trim(); + } + + public String getEnvBuildDate() { + return context.getSetting("edc.build.date", ""); + } + + public LastCommitInfo getLastCommitInfo() { + var lastCommitInfo = new LastCommitInfo(); + lastCommitInfo.setEnvLastCommitInfo(getEnvLastCommitInfo()); + lastCommitInfo.setJarLastCommitInfo(getJarLastCommitInfo()); + + lastCommitInfo.setJarBuildDate(getJarBuildDate()); + lastCommitInfo.setEnvBuildDate(getEnvBuildDate()); + return lastCommitInfo; + } +} + + + + + diff --git a/extensions/last-commit-info/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/last-commit-info/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..cef3b4bbd --- /dev/null +++ b/extensions/last-commit-info/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.LastCommitInfoExtension diff --git a/extensions/last-commit-info/src/main/resources/jar-build-date.txt b/extensions/last-commit-info/src/main/resources/jar-build-date.txt new file mode 100644 index 000000000..e3a56bf7d --- /dev/null +++ b/extensions/last-commit-info/src/main/resources/jar-build-date.txt @@ -0,0 +1 @@ +The GitHub Pipeline will replace this file during build time with the current date. diff --git a/extensions/last-commit-info/src/main/resources/jar-last-commit-info.txt b/extensions/last-commit-info/src/main/resources/jar-last-commit-info.txt new file mode 100644 index 000000000..f63c928a9 --- /dev/null +++ b/extensions/last-commit-info/src/main/resources/jar-last-commit-info.txt @@ -0,0 +1,3 @@ +The GitHub Pipeline will replace this file during build time with +the commit information (SHA, author and commit message) of the built image. +This file will be served to allow checking for the exact source code version of a deployed edc-ui. diff --git a/extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/LastCommitInfoTest.java b/extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/LastCommitInfoTest.java new file mode 100644 index 000000000..6219e5a61 --- /dev/null +++ b/extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/LastCommitInfoTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.version.controller; + +import io.restassured.http.ContentType; +import org.eclipse.edc.connector.dataplane.selector.spi.store.DataPlaneInstanceStore; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.protocol.ProtocolWebhook; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +@ApiTest +@ExtendWith(EdcExtension.class) +class LastCommitInfoTest { + + @BeforeEach + void setUp(EdcExtension extension) { + extension.registerServiceMock(ProtocolWebhook.class, mock(ProtocolWebhook.class)); + extension.registerServiceMock(JsonLd.class, mock(JsonLd.class)); + extension.registerServiceMock( + DataPlaneInstanceStore.class, + mock(DataPlaneInstanceStore.class)); + extension.setConfiguration(Map.of( + "web.http.port", String.valueOf(getFreePort()), + "web.http.path", "/api", + "web.http.management.port", String.valueOf(TestUtils.DATA_PORT), + "web.http.management.path", "/api/v1/data", + "edc.api.auth.key", TestUtils.AUTH_KEY, + "edc.last.commit.info", "test env commit message", + "edc.build.date", "2023-05-08T15:30:00Z")); + } + + @Test + void testEnvAndJar() { + var request = given() + .baseUri("http://localhost:" + TestUtils.DATA_PORT) + .basePath("/api/v1/data") + .header("X-Api-Key", TestUtils.AUTH_KEY) + .when() + .contentType(ContentType.JSON) + .get("/last-commit-info") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + + request.assertThat().body("envLastCommitInfo", equalTo("test env commit message")) + .body("envBuildDate", equalTo("2023-05-08T15:30:00Z")) + .body("jarLastCommitInfo", equalTo("test jar commit message")) + .body("jarBuildDate", equalTo("2023-05-09T15:30:00Z")); + + } +} diff --git a/extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java b/extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java new file mode 100644 index 000000000..92ccecf04 --- /dev/null +++ b/extensions/last-commit-info/src/test/java/de/sovity/edc/extension/version/controller/TestUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.version.controller; + +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; + + +public class TestUtils { + + public static final String AUTH_KEY = "123456"; + public static final int DATA_PORT = getFreePort(); + + +} diff --git a/extensions/last-commit-info/src/test/resources/jar-build-date.txt b/extensions/last-commit-info/src/test/resources/jar-build-date.txt new file mode 100644 index 000000000..3f3faacba --- /dev/null +++ b/extensions/last-commit-info/src/test/resources/jar-build-date.txt @@ -0,0 +1 @@ +2023-05-09T15:30:00Z diff --git a/extensions/last-commit-info/src/test/resources/jar-last-commit-info.txt b/extensions/last-commit-info/src/test/resources/jar-last-commit-info.txt new file mode 100644 index 000000000..1516a70eb --- /dev/null +++ b/extensions/last-commit-info/src/test/resources/jar-last-commit-info.txt @@ -0,0 +1 @@ +test jar commit message diff --git a/extensions/policy-always-true/README.md b/extensions/policy-always-true/README.md new file mode 100644 index 000000000..fe61a0f87 --- /dev/null +++ b/extensions/policy-always-true/README.md @@ -0,0 +1,31 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Always True Policy

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension +This extension creates a Policy Definition `always-true` on EDC startup. + +## Why does this extension exist? + +While the default behavior for contract definitions with empty policies is not "default deny", +our UI will be ensuring non-empty access and contract policies. + +Therefore, it is of interest to have an `always-true` policy to explicitly enable full access in contract definitions. + +## License +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact +sovity GmbH - contact@sovity.de diff --git a/extensions/policy-always-true/build.gradle.kts b/extensions/policy-always-true/build.gradle.kts new file mode 100644 index 000000000..0b1fc415f --- /dev/null +++ b/extensions/policy-always-true/build.gradle.kts @@ -0,0 +1,33 @@ +val edcVersion: String by project +val edcGroup: String by project +val mockitoVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:policy-engine-spi:${edcVersion}") + api("${edcGroup}:control-plane-spi:${edcVersion}") + implementation("${edcGroup}:api-core:${edcVersion}") + + testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyConstants.java b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyConstants.java new file mode 100644 index 000000000..ec9115b19 --- /dev/null +++ b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy; + +public class AlwaysTruePolicyConstants { + public static final String EXTENSION_NAME = "Policy Function: ALWAYS_TRUE"; + public static final String EXPRESSION_LEFT_VALUE = "ALWAYS_TRUE"; + public static final String EXPRESSION_RIGHT_VALUE = "true"; + public static final String POLICY_DEFINITION_ID = "always-true"; + + private AlwaysTruePolicyConstants() { + } +} diff --git a/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtension.java b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtension.java new file mode 100644 index 000000000..a653d90e9 --- /dev/null +++ b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtension.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy; + +import de.sovity.edc.extension.policy.services.AlwaysTruePolicyDefinitionService; +import de.sovity.edc.extension.policy.services.AlwaysTruePolicyService; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.EXTENSION_NAME; + +/** + * Extension: Policy Definition "Always True". + */ +public class AlwaysTruePolicyExtension implements ServiceExtension { + @Inject + private Monitor monitor; + + @Inject + private RuleBindingRegistry ruleBindingRegistry; + + @Inject + private PolicyDefinitionService policyDefinitionService; + + @Inject + private PolicyEngine policyEngine; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var alwaysTruePolicyService = new AlwaysTruePolicyService(ruleBindingRegistry, policyEngine); + alwaysTruePolicyService.registerPolicy(); + } + + @Override + public void start() { + var alwaysTruePolicyDefinitionService = new AlwaysTruePolicyDefinitionService(policyDefinitionService); + if (!alwaysTruePolicyDefinitionService.exists()) { + monitor.info("Creating Always True Policy Definition."); + alwaysTruePolicyDefinitionService.create(); + } else { + monitor.debug("Skipping Always True Policy Definition creation, policy definition already exists."); + } + } +} diff --git a/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java new file mode 100644 index 000000000..101b7364e --- /dev/null +++ b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy.services; + +import org.eclipse.edc.connector.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.LiteralExpression; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; + +import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE; +import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.EXPRESSION_RIGHT_VALUE; +import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; + +/** + * Creates policy definition "always-true". + */ +public class AlwaysTruePolicyDefinitionService { + private final PolicyDefinitionService policyDefinitionService; + + public AlwaysTruePolicyDefinitionService(PolicyDefinitionService policyDefinitionService) { + this.policyDefinitionService = policyDefinitionService; + } + + /** + * Checks if policy definition "always-true" exists + * + * @return if exists + */ + public boolean exists() { + return policyDefinitionService.findById(POLICY_DEFINITION_ID) != null; + } + + /** + * Creates policy definition "always-true". + */ + public void create() { + var alwaysTrueConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression(EXPRESSION_LEFT_VALUE)) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression(EXPRESSION_RIGHT_VALUE)) + .build(); + var alwaysTruePermission = Permission.Builder.newInstance() + .action(Action.Builder.newInstance().type("USE").build()) + .constraint(alwaysTrueConstraint) + .build(); + var policy = Policy.Builder.newInstance() + .permission(alwaysTruePermission) + .build(); + var policyDefinition = PolicyDefinition.Builder.newInstance() + .id(POLICY_DEFINITION_ID) + .policy(policy) + .build(); + policyDefinitionService.create(policyDefinition); + } +} diff --git a/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyService.java b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyService.java new file mode 100644 index 000000000..7c1b82ae3 --- /dev/null +++ b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyService.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy.services; + +import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; + +import static org.eclipse.edc.policy.engine.spi.PolicyEngine.ALL_SCOPES; + +/** + * Creates policy "Always True". + *

      + * To be exact, it resolves to true iff constraint is {@link AlwaysTruePolicyConstants#EXPRESSION_LEFT_VALUE} + * "EQ" {@link AlwaysTruePolicyConstants#EXPRESSION_RIGHT_VALUE}. + */ +public class AlwaysTruePolicyService { + private final RuleBindingRegistry ruleBindingRegistry; + private final PolicyEngine policyEngine; + + public AlwaysTruePolicyService(RuleBindingRegistry ruleBindingRegistry, PolicyEngine policyEngine) { + this.ruleBindingRegistry = ruleBindingRegistry; + this.policyEngine = policyEngine; + } + + public void registerPolicy() { + ruleBindingRegistry.bind("USE", ALL_SCOPES); + ruleBindingRegistry.bind(AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE, ALL_SCOPES); + policyEngine.registerFunction( + ALL_SCOPES, + Permission.class, + AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE, + (operator, rightValue, rule, context1) -> operator.equals(Operator.EQ) && + rightValue.toString().equals(AlwaysTruePolicyConstants.EXPRESSION_RIGHT_VALUE) + ); + } + +} diff --git a/extensions/policy-always-true/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/policy-always-true/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..1882fd0b2 --- /dev/null +++ b/extensions/policy-always-true/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.policy.AlwaysTruePolicyExtension diff --git a/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java b/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java new file mode 100644 index 000000000..6824ef12c --- /dev/null +++ b/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy; + +import org.eclipse.edc.connector.contract.spi.offer.ContractDefinitionResolver; +import org.eclipse.edc.connector.dataplane.selector.spi.store.DataPlaneInstanceStore; +import org.eclipse.edc.connector.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.eclipse.edc.spi.protocol.ProtocolWebhook; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@ApiTest +@ExtendWith(EdcExtension.class) +class AlwaysTruePolicyExtensionTest { + + @BeforeEach + void setUp(EdcExtension extension) { + extension.registerServiceMock(ProtocolWebhook.class, mock(ProtocolWebhook.class)); + extension.registerServiceMock( + DataPlaneInstanceStore.class, + mock(DataPlaneInstanceStore.class)); + } + + @Test + void alwaysTruePolicyDef(PolicyEngine policyEngine, + PolicyDefinitionService policyDefinitionService) { + // arrange + var alwaysTrue = alwaysTruePolicy(policyDefinitionService); + + // act + var result = policyEngine.evaluate( + ContractDefinitionResolver.CATALOGING_SCOPE, + alwaysTrue.getPolicy(), + participantAgent() + ); + + // assert + assertTrue(result.succeeded(), "Always True Policy wasn't true."); + } + + @NotNull + private PolicyDefinition alwaysTruePolicy(PolicyDefinitionService policyDefinitionService) { + var alwaysTrue = policyDefinitionService.findById(POLICY_DEFINITION_ID); + requireNonNull(alwaysTrue, "Policy Definition does not exist: " + POLICY_DEFINITION_ID); + return alwaysTrue; + } + + @NotNull + private ParticipantAgent participantAgent() { + return new ParticipantAgent(Map.of(), Map.of()); + } +} diff --git a/extensions/policy-referring-connector/README.md b/extensions/policy-referring-connector/README.md new file mode 100644 index 000000000..b6c97689a --- /dev/null +++ b/extensions/policy-referring-connector/README.md @@ -0,0 +1,36 @@ + +
      +

      + + Logo + + +

      EDC-Connector Extension:
      Referring Connector Restricted Policy

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension + +This extension adds a policy function that allows validating the consuming EDC Connector's DAT claim +value `referringConnector`. This allows to limit data-offer access to specific consumers. + +Adds permission function with left side expression `REFERRING_CONNECTOR` with the currently supported +`EQ` operator. + +## Why does this extension exist? + +Especially in data spaces where data is shared with business partners for specific purposes, data assets need to +be offered to desired consumers only. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/policy-referring-connector/build.gradle.kts b/extensions/policy-referring-connector/build.gradle.kts new file mode 100644 index 000000000..972612083 --- /dev/null +++ b/extensions/policy-referring-connector/build.gradle.kts @@ -0,0 +1,35 @@ +val edcVersion: String by project +val edcGroup: String by project +val mockitoVersion: String by project +val jupiterVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api("${edcGroup}:auth-spi:${edcVersion}") + api("${edcGroup}:policy-engine-spi:${edcVersion}") + api("${edcGroup}:contract-spi:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-params:${jupiterVersion}") +} + +tasks.withType { + useJUnitPlatform() +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtension.java b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtension.java new file mode 100644 index 000000000..61033b8c2 --- /dev/null +++ b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtension.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy; + +import de.sovity.edc.extension.policy.functions.ReferringConnectorDutyFunction; +import de.sovity.edc.extension.policy.functions.ReferringConnectorPermissionFunction; +import de.sovity.edc.extension.policy.functions.ReferringConnectorProhibitionFunction; +import org.eclipse.edc.connector.contract.spi.offer.ContractDefinitionResolver; +import org.eclipse.edc.connector.contract.spi.validation.ContractValidationService; +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.policy.model.Rule; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static org.eclipse.edc.policy.engine.spi.PolicyEngine.ALL_SCOPES; + +public class ReferringConnectorValidationExtension implements ServiceExtension { + + /** + * The key for referring connector constraints. + * Must be used as left operand when declaring constraints. + * rightOperand can be a string-URL or a comma separated list of string-URLs. + * Also supports the IN Operator with a list of string-URLs as right operand. + * + *

      Example: + * + *

      +     * {
      +     *     "constraint": {
      +     *         "leftOperand": "REFERRING_CONNECTOR",
      +     *         "operator": "EQ",
      +     *         "rightOperand": "http://example.org,http://example.org"
      +     *     }
      +     * }
      +     * 
      + * + * Constraint: + *
      +     *       {
      +     *         "edctype": "AtomicConstraint",
      +     *         "leftExpression": {
      +     *           "edctype": "dataspaceconnector:literalexpression",
      +     *           "value": "REFERRING_CONNECTOR"
      +     *         },
      +     *         "rightExpression": {
      +     *           "edctype": "dataspaceconnector:literalexpression",
      +     *           "value": "http://example.org"
      +     *         },
      +     *         "operator": "EQ"
      +     *       }
      +     * 
      + */ + public static final String REFERRING_CONNECTOR_CONSTRAINT_KEY = "REFERRING_CONNECTOR"; + + public ReferringConnectorValidationExtension() {} + + public ReferringConnectorValidationExtension(final RuleBindingRegistry ruleBindingRegistry, + final PolicyEngine policyEngine) { + this.ruleBindingRegistry = ruleBindingRegistry; + this.policyEngine = policyEngine; + } + + @Inject + private RuleBindingRegistry ruleBindingRegistry; + + @Inject + private PolicyEngine policyEngine; + + @Override + public String name() { + return "Policy Function: REFERRING_CONNECTOR"; + } + + @Override + public void initialize(ServiceExtensionContext context) { + ruleBindingRegistry.bind(REFERRING_CONNECTOR_CONSTRAINT_KEY, ContractValidationService.NEGOTIATION_SCOPE); + ruleBindingRegistry.bind(REFERRING_CONNECTOR_CONSTRAINT_KEY, ContractDefinitionResolver.CATALOGING_SCOPE); + + var monitor = context.getMonitor(); + registerPolicyFunction(Duty.class, new ReferringConnectorDutyFunction(monitor)); + registerPolicyFunction(Permission.class, new ReferringConnectorPermissionFunction(monitor)); + registerPolicyFunction(Prohibition.class, new ReferringConnectorProhibitionFunction(monitor)); + } + + private void registerPolicyFunction(Class type, AtomicConstraintFunction function) { + policyEngine.registerFunction(ALL_SCOPES, type, REFERRING_CONNECTOR_CONSTRAINT_KEY, function); + } +} diff --git a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java new file mode 100644 index 000000000..8fb09c350 --- /dev/null +++ b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy.functions; + +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Abstract class for referringConnector validation. This class may be inherited from the EDC + * policy enforcing functions for duties, permissions and prohibitions. + */ +public abstract class AbstractReferringConnectorValidation { + private static final String FAIL_EVALUATION_BECAUSE_RIGHT_VALUE_NOT_STRING = + "Failing evaluation because of invalid referring connector constraint. For operator 'EQ' right value must be of type 'String'. Unsupported type: '%s'"; + private static final String FAIL_EVALUATION_BECAUSE_UNSUPPORTED_OPERATOR = + "Failing evaluation because of invalid referring connector constraint. Unsupported operator: '%s'"; + + private final Monitor monitor; + + protected AbstractReferringConnectorValidation(Monitor monitor) { + this.monitor = Objects.requireNonNull(monitor); + } + + private static final String REFERRING_CONNECTOR_CLAIM = "referringConnector"; + + /** + * Evaluation function. + * + * @param operator operator of the constraint + * @param rightValue right value fo the constraint, that contains a referring connector + * @param policyContext context of the policy with claims + * @return true if claims are from the constrained referring connector + */ + protected boolean evaluate(final Operator operator, final Object rightValue, final PolicyContext policyContext) { + if (policyContext.hasProblems() && !policyContext.getProblems().isEmpty()) { + var problems = String.join(", ", policyContext.getProblems()); + var message = + String.format( + "ReferringConnectorValidation: Rejecting PolicyContext with problems. Problems: %s", + problems); + monitor.debug(message); + return false; + } + + final var claims = policyContext.getParticipantAgent().getClaims(); + + if (!claims.containsKey(REFERRING_CONNECTOR_CLAIM)) { + return false; + } + + Object referringConnectorClaimObject = claims.get(REFERRING_CONNECTOR_CLAIM); + String referringConnectorClaim = null; + + if (referringConnectorClaimObject instanceof String string) { + referringConnectorClaim = string; + } + + if (referringConnectorClaim == null || referringConnectorClaim.isEmpty()) { + return false; + } + + if (operator == Operator.EQ || operator == Operator.IN) { + return isReferringConnector(referringConnectorClaim, rightValue, policyContext, operator); + } else { + final var message = String.format(FAIL_EVALUATION_BECAUSE_UNSUPPORTED_OPERATOR, operator); + monitor.warning(message); + policyContext.reportProblem(message); + return false; + } + } + + /** + * Validates if value set in policy and if value interpretable. + * + * @param referringConnectorClaim of the participant + * @param referringConnector object of rightValue of constraint + * @return true if object is string and successfully evaluated against the claim + */ + private boolean isReferringConnector( + String referringConnectorClaim, Object referringConnector, PolicyContext policyContext, Operator operator) { + //no right value set in policy + if (referringConnector == null) { + final var message = String.format(FAIL_EVALUATION_BECAUSE_RIGHT_VALUE_NOT_STRING, "null"); + monitor.warning(message); + policyContext.reportProblem(message); + return false; + } + + //evaluate + return isAllowedReferringConnector(referringConnectorClaim, referringConnector, operator); + } + + /** + * Main evaluation, evaluates if claim is allowed referringConnector. + * + * @param referringConnectorClaim of the participant + * @param referringConnector object of rightValue of constraint + * @return true if claim equals the referringConnector + */ + private static boolean isAllowedReferringConnector( + String referringConnectorClaim, Object referringConnector, Operator operator) { + if (operator == Operator.IN) { + var referringConnectorList = (List) referringConnector; + return referringConnectorList.contains(referringConnectorClaim); + } else if (operator == Operator.EQ) { + //support comma separated lists here as well + if (referringConnector instanceof String referringConnectorString) { + return Arrays.asList(referringConnectorString.split(",")).contains(referringConnectorClaim); + } + } + return false; + } +} diff --git a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorDutyFunction.java b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorDutyFunction.java new file mode 100644 index 000000000..5c11c57ef --- /dev/null +++ b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorDutyFunction.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy.functions; + +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.monitor.Monitor; + +/** AtomicConstraintFunction to validate the referring connector claim for edc duties. */ +public class ReferringConnectorDutyFunction extends AbstractReferringConnectorValidation + implements AtomicConstraintFunction { + + public ReferringConnectorDutyFunction(Monitor monitor) { + super(monitor); + } + + @Override + public boolean evaluate(Operator operator, Object rightValue, Duty rule, PolicyContext context) { + return evaluate(operator, rightValue, context); + } +} \ No newline at end of file diff --git a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorPermissionFunction.java b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorPermissionFunction.java new file mode 100644 index 000000000..2e1a274a5 --- /dev/null +++ b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorPermissionFunction.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy.functions; + +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.spi.monitor.Monitor; + +/** AtomicConstraintFunction to validate the referring connector claim for edc permissions. */ +public class ReferringConnectorPermissionFunction extends AbstractReferringConnectorValidation + implements AtomicConstraintFunction { + + public ReferringConnectorPermissionFunction(Monitor monitor) { + super(monitor); + } + + @Override + public boolean evaluate(Operator operator, Object rightValue, Permission rule, PolicyContext context) { + return evaluate(operator, rightValue, context); + } +} \ No newline at end of file diff --git a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorProhibitionFunction.java b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorProhibitionFunction.java new file mode 100644 index 000000000..b0ef032d7 --- /dev/null +++ b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/ReferringConnectorProhibitionFunction.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy.functions; + +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.spi.monitor.Monitor; + +/** AtomicConstraintFunction to validate the referring connector claim edc prohibitions. */ +public class ReferringConnectorProhibitionFunction extends AbstractReferringConnectorValidation + implements AtomicConstraintFunction { + + public ReferringConnectorProhibitionFunction(Monitor monitor) { + super(monitor); + } + + @Override + public boolean evaluate(Operator operator, Object rightValue, Prohibition rule, PolicyContext context) { + return evaluate(operator, rightValue, context); + } +} \ No newline at end of file diff --git a/extensions/policy-referring-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/policy-referring-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..5ccd1755b --- /dev/null +++ b/extensions/policy-referring-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,23 @@ +# +# Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH +# Copyright (c) 2021,2022 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# sovity GmbH - adaptation and modifications +# +de.sovity.edc.extension.policy.ReferringConnectorValidationExtension \ No newline at end of file diff --git a/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtensionTest.java b/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtensionTest.java new file mode 100644 index 000000000..846c8f00a --- /dev/null +++ b/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/ReferringConnectorValidationExtensionTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy; + +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class ReferringConnectorValidationExtensionTest { + + private ReferringConnectorValidationExtension extension; + + // mocks + private ServiceExtensionContext serviceExtensionContext; + private PolicyEngine policyEngine; + private RuleBindingRegistry ruleBindingRegistry; + + @BeforeEach + void setup() { + + policyEngine = Mockito.mock(PolicyEngine.class); + ruleBindingRegistry = Mockito.mock(RuleBindingRegistry.class); + + final Monitor monitor = Mockito.mock(Monitor.class); + serviceExtensionContext = Mockito.mock(ServiceExtensionContext.class); + + Mockito.when(serviceExtensionContext.getMonitor()).thenReturn(monitor); + + extension = new ReferringConnectorValidationExtension(ruleBindingRegistry, policyEngine); + } + + @Test + void testRegisterDutyFunction() { + + // invoke + extension.initialize(serviceExtensionContext); + + // verify + Mockito.verify(policyEngine, Mockito.times(1)) + .registerFunction( + Mockito.anyString(), + Mockito.eq(Duty.class), + Mockito.eq(ReferringConnectorValidationExtension.REFERRING_CONNECTOR_CONSTRAINT_KEY), + Mockito.any()); + } + + @Test + void testRegisterPermissionFunction() { + + // invoke + extension.initialize(serviceExtensionContext); + + // verify + Mockito.verify(policyEngine, Mockito.times(1)) + .registerFunction( + Mockito.anyString(), + Mockito.eq(Permission.class), + Mockito.eq(ReferringConnectorValidationExtension.REFERRING_CONNECTOR_CONSTRAINT_KEY), + Mockito.any()); + } + + @Test + void testRegisterProhibitionFunction() { + + // invoke + extension.initialize(serviceExtensionContext); + + // verify + Mockito.verify(policyEngine, Mockito.times(1)) + .registerFunction( + Mockito.anyString(), + Mockito.eq(Prohibition.class), + Mockito.eq(ReferringConnectorValidationExtension.REFERRING_CONNECTOR_CONSTRAINT_KEY), + Mockito.any()); + } +} \ No newline at end of file diff --git a/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java b/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java new file mode 100644 index 000000000..31d072abc --- /dev/null +++ b/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * Copyright (c) 2021,2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - adaptation and modifications + * + */ + +package de.sovity.edc.extension.policy.functions; + +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mockito; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AbstractReferringConnectorValidationTest { + + private AbstractReferringConnectorValidation validation; + + // mocks + private Monitor monitor; + private PolicyContext policyContext; + private ParticipantAgent participantAgent; + + @BeforeEach + void beforeEach() { + this.monitor = Mockito.mock(Monitor.class); + this.policyContext = Mockito.mock(PolicyContext.class); + this.participantAgent = Mockito.mock(ParticipantAgent.class); + + Mockito.when(policyContext.getParticipantAgent()).thenReturn(participantAgent); + + validation = new AbstractReferringConnectorValidation(monitor) {}; + } + + @ParameterizedTest + @EnumSource(Operator.class) + void testFailsOnUnsupportedOperations(Operator operator) { + + if (operator == Operator.EQ || operator == Operator.IN) { + return; + } + + // prepare + prepareContextProblems(null); + prepareReferringConnectorClaim("http://example.org"); + + // invoke & assert + assertFalse(validation.evaluate(operator, "foo", policyContext)); + } + + @Test + void testFailsOnUnsupportedRightValue() { + + // prepare + prepareContextProblems(null); + prepareReferringConnectorClaim("http://example.org"); + + // invoke & assert + assertFalse(validation.evaluate(Operator.EQ, 1, policyContext)); + } + + @Test + void testValidationFailsWhenClaimMissing() { + + // prepare + prepareContextProblems(null); + + // invoke + final boolean isValid = validation.evaluate(Operator.EQ, "http://example.org", policyContext); + + // assert + assertFalse(isValid); + } + + @Test + void testValidationWhenClaimContainsValue() { + + // prepare + prepareContextProblems(null); + + // prepare equals + prepareReferringConnectorClaim("http://example.org"); + final boolean isEqualsTrue = validation.evaluate(Operator.EQ, "http://example.org", policyContext); + + // prepare contains + prepareReferringConnectorClaim("http://example.com/http://example.org"); + final boolean isContainedTrue = validation.evaluate(Operator.EQ, "http://example.org", policyContext); + + // assert + assertTrue(isEqualsTrue); + assertFalse(isContainedTrue); + } + + @Test + void testValidationWhenClaimContainsValueAsCommaList() { + + // prepare + prepareContextProblems(null); + + // prepare equals + prepareReferringConnectorClaim("http://example.org"); + final boolean isEqualsTrue = validation.evaluate(Operator.EQ, "http://example2.org,http://example.org", policyContext); + + // prepare contains + prepareReferringConnectorClaim("http://example.com/http://example.org"); + final boolean isContainedTrue = validation.evaluate(Operator.EQ, "http://example.org", policyContext); + + // assert + assertTrue(isEqualsTrue); + assertFalse(isContainedTrue); + } + + @Test + void testValidationWhenParticipantHasProblems() { + + // prepare + prepareContextProblems(Collections.singletonList("big problem")); + prepareReferringConnectorClaim("http://example.org"); + + // invoke + final boolean isValid = validation.evaluate(Operator.EQ, "http://example.org", policyContext); + + // Mockito.verify(monitor.debug(Mockito.anyString()); + assertFalse(isValid); + } + + @Test + void testValidationWhenSingleParticipantIsValid() { + + // prepare + prepareContextProblems(null); + prepareReferringConnectorClaim("http://example.org"); + + // invoke + final boolean isContainedTrue = validation.evaluate(Operator.EQ, "http://example.org", policyContext); + + // Mockito.verify(monitor.debug(Mockito.anyString()); + assertTrue(isContainedTrue); + } + + @Test + void testValidationForMultipleParticipants() { + // prepare + prepareContextProblems(null); + prepareReferringConnectorClaim("http://example.org"); + + // invoke & verify + assertTrue(validation.evaluate(Operator.IN, List.of("http://example.org", "http://example.com"), policyContext)); + assertTrue(validation.evaluate(Operator.IN, List.of(1, "http://example.org"), policyContext)); + assertTrue(validation.evaluate(Operator.IN, List.of("http://example.org", "http://example.org"), policyContext)); + } + + private void prepareContextProblems(List problems) { + Mockito.when(policyContext.getProblems()).thenReturn(problems); + + if (problems == null || problems.isEmpty()) { + Mockito.when(policyContext.hasProblems()).thenReturn(false); + } else { + Mockito.when(policyContext.hasProblems()).thenReturn(true); + } + } + + private void prepareReferringConnectorClaim(String referringConnector) { + Mockito.when(participantAgent.getClaims()) + .thenReturn(Collections.singletonMap("referringConnector", referringConnector)); + } +} diff --git a/extensions/policy-time-interval/README.md b/extensions/policy-time-interval/README.md new file mode 100644 index 000000000..e6c543c77 --- /dev/null +++ b/extensions/policy-time-interval/README.md @@ -0,0 +1,35 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Time Interval Restricted Policy

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension + +This extension adds a policy function that validates the time of data consumption. + +Adds permission function with left side expression `POLICY_EVALUATION_TIME` and supported +operators `EQ`, `NEQ`, `LT`, `LEQ`, `GT`, `GEQ`. The right side expression is expected to be in the following date +format `yyyy-MM-dd'T'HH:mm:ss.SSSXXX`. + +## Why does this extension exist? + +Limiting data offers to specific valid durations. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/policy-time-interval/build.gradle.kts b/extensions/policy-time-interval/build.gradle.kts new file mode 100644 index 000000000..4a79097d7 --- /dev/null +++ b/extensions/policy-time-interval/build.gradle.kts @@ -0,0 +1,24 @@ +val edcVersion: String by project +val edcGroup: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api("${edcGroup}:auth-spi:${edcVersion}") + api("${edcGroup}:policy-engine-spi:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeExtension.java b/extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeExtension.java new file mode 100644 index 000000000..feb80ae4e --- /dev/null +++ b/extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeExtension.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy; + +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static org.eclipse.edc.policy.engine.spi.PolicyEngine.ALL_SCOPES; + +public class PolicyEvaluationTimeExtension implements ServiceExtension { + + private static final String KEY_POLICY_EVALUATION_TIME = "POLICY_EVALUATION_TIME"; + private static final String EXTENSION_NAME = "Policy Function: POLICY_EVALUATION_TIME"; + + @Inject + private RuleBindingRegistry ruleBindingRegistry; + + @Inject + private PolicyEngine policyEngine; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + + ruleBindingRegistry.bind("USE", ALL_SCOPES); + ruleBindingRegistry.bind(KEY_POLICY_EVALUATION_TIME, ALL_SCOPES); + policyEngine.registerFunction( + ALL_SCOPES, + Permission.class, + KEY_POLICY_EVALUATION_TIME, + new PolicyEvaluationTimeFunction(monitor)); + } +} diff --git a/extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeFunction.java b/extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeFunction.java new file mode 100644 index 000000000..0a461272c --- /dev/null +++ b/extensions/policy-time-interval/src/main/java/de/sovity/edc/extension/policy/PolicyEvaluationTimeFunction.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.policy; + +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; + +public class PolicyEvaluationTimeFunction implements AtomicConstraintFunction { + private final Monitor monitor; + + public PolicyEvaluationTimeFunction(Monitor monitor) { + this.monitor = monitor; + } + + @Override + public boolean evaluate(Operator operator, Object rightValue, Permission rule, PolicyContext context) { + try { + var policyDate = OffsetDateTime.parse((String) rightValue); + var nowDate = OffsetDateTime.now(); + return switch (operator) { + case LT -> nowDate.isBefore(policyDate); + case LEQ -> nowDate.isBefore(policyDate) || nowDate.equals(policyDate); + case GT -> nowDate.isAfter(policyDate); + case GEQ -> nowDate.isAfter(policyDate) || nowDate.equals(policyDate); + case EQ -> nowDate.equals(policyDate); + case NEQ -> !nowDate.equals(policyDate); + default -> false; + }; + } catch (DateTimeParseException e) { + monitor.severe("Failed to parse right value of constraint to date."); + return false; + } + } +} diff --git a/extensions/policy-time-interval/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/policy-time-interval/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..4deab3d3f --- /dev/null +++ b/extensions/policy-time-interval/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.policy.PolicyEvaluationTimeExtension diff --git a/extensions/postgres-flyway/README.md b/extensions/postgres-flyway/README.md new file mode 100644 index 000000000..a62f29b4e --- /dev/null +++ b/extensions/postgres-flyway/README.md @@ -0,0 +1,62 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      PostgreSQL + Flyway

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension Package + +This extension bundles all functionalities for using the EDC with PostgreSQL persistence. It also includes the required +Flyway migrations and extensions. + +### Details + +The extension includes the edc-stores for the following edc-types: + +- asset +- contractdefinition +- contractnegotiation +- dataplaneinstance +- policy +- transferprocess + +Futhermore, the `ConnectionsPool`, `transaction`-Extensions and the JDBC-Driver for the +PostgreSQL-Database are provided. + +The tables are prepared using Flyway, which executes the .sql scripts included in +the `resources/migration` folder. + +There are Sovity EDC Community Edition specific migration scripts in the folder `resources/migration/default`. + +### Configuration + +Additional Migration Scripts can be added by specifiying the configuration property +`edc.flyway.additional.migration.locations`. Values are comma separated and need to be correct [FlyWay migration +script locations](https://flywaydb.org/documentation/configuration/parameters/locations). These migration scripts need +to be compatible to the migrations in `resources/migration/default`. + +For further configuration options, please refer to the configuration of our sovity Community Edition EDC and its .env +file. + +## Why does this extension exist? + +While the EDC is providing capabilities for individual persistence stores, our goal is to provide a single working +extension package to allow switching to a well-managed PostgreSQL persistence. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/postgres-flyway/build.gradle.kts b/extensions/postgres-flyway/build.gradle.kts new file mode 100644 index 000000000..203782838 --- /dev/null +++ b/extensions/postgres-flyway/build.gradle.kts @@ -0,0 +1,42 @@ +val edcVersion: String by project +val edcGroup: String by project +val tractusVersion: String by project +val tractusGroup: String by project +val flywayVersion: String by project +val postgresVersion: String by project +val lombokVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + implementation("${edcGroup}:core-spi:${edcVersion}") + implementation("${edcGroup}:sql-core:${edcVersion}") + + // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) + implementation("${edcGroup}:control-plane-sql:${edcVersion}") + implementation("${tractusGroup}:sql-pool:${tractusVersion}") + implementation("${edcGroup}:transaction-local:${edcVersion}") + + implementation("org.postgresql:postgresql:${postgresVersion}") + + implementation("org.flywaydb:flyway-core:${flywayVersion}") + + testImplementation("${edcGroup}:junit:${edcVersion}") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java new file mode 100644 index 000000000..e44b528b6 --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql; + +import de.sovity.edc.extension.postgresql.migration.DatabaseMigrationManager; +import de.sovity.edc.extension.postgresql.migration.FlywayService; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +public class PostgresFlywayExtension implements ServiceExtension { + + + @Setting + public static final String EDC_DATASOURCE_REPAIR_SETTING = "edc.flyway.repair"; + @Setting + public static final String FLYWAY_CLEAN_ENABLED = "edc.flyway.clean.enable"; + @Setting + public static final String FLYWAY_CLEAN = "edc.flyway.clean"; + + @Override + public String name() { + return "Postgres Flyway Extension"; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var flywayService = new FlywayService( + context.getMonitor(), + context.getSetting(EDC_DATASOURCE_REPAIR_SETTING, false), + context.getSetting(FLYWAY_CLEAN_ENABLED, false), + context.getSetting(FLYWAY_CLEAN, false) + ); + var migrationManager = new DatabaseMigrationManager(context.getConfig(), flywayService); + migrationManager.migrateAllDataSources(); + } + +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java new file mode 100644 index 000000000..64016df9b --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql.connection; + +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.sql.ConnectionFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +public class DriverManagerConnectionFactory implements ConnectionFactory { + + private static final String CONNECTION_PROPERTY_USER = "user"; + private static final String CONNECTION_PROPERTY_PASSWORD = "password"; + + private final JdbcConnectionProperties jdbcProperties; + + public DriverManagerConnectionFactory(JdbcConnectionProperties jdbcProperties) { + this.jdbcProperties = jdbcProperties; + } + + @Override + public Connection create() { + try { + var properties = new Properties(); + properties.setProperty(CONNECTION_PROPERTY_USER, jdbcProperties.getUser()); + properties.setProperty(CONNECTION_PROPERTY_PASSWORD, jdbcProperties.getPassword()); + return DriverManager.getConnection(jdbcProperties.getJdbcUrl(), properties); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java new file mode 100644 index 000000000..b8fcb8141 --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql.connection; + +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.configuration.Config; + +public class JdbcConnectionProperties { + + @Setting(required = true) + private static final String DATASOURCE_SETTING_JDBC_URL = "edc.datasource.%s.url"; + @Setting(required = true) + private static final String DATASOURCE_SETTING_USER = "edc.datasource.%s.user"; + @Setting(required = true) + private static final String DATASOURCE_SETTING_PASSWORD = "edc.datasource.%s.password"; + + private final String jdbcUrl; + private final String user; + private final String password; + + public JdbcConnectionProperties(Config config, String entityName) { + jdbcUrl = config.getString(String.format(DATASOURCE_SETTING_JDBC_URL, entityName)); + user = config.getString(String.format(DATASOURCE_SETTING_USER, entityName)); + password = config.getString(String.format(DATASOURCE_SETTING_PASSWORD, entityName)); + } + + public String getJdbcUrl() { + return jdbcUrl; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java new file mode 100644 index 000000000..71794207d --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql.migration; + +import de.sovity.edc.extension.postgresql.connection.JdbcConnectionProperties; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.util.Arrays; +import java.util.List; + +public class DatabaseMigrationManager { + @Setting + public static final String EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS = "edc.flyway.additional.migration.locations"; + private static final String EDC_DATASOURCE_PREFIX = "edc.datasource"; + private static final String DEFAULT_DATASOURCE = "default"; + + private final Config config; + private final FlywayService flywayService; + + private final List dataSourceNames = List.of( + // Pre EDC 0 legacy migrations + "asset", + "contractdefinition", + "policy", + "contractnegotiation", + "transferprocess", + "dataplaneinstance", + + // Actual DB migrations + "default" + ); + + public DatabaseMigrationManager(Config config, FlywayService flywayService) { + this.config = config; + this.flywayService = flywayService; + } + + public void migrateAllDataSources() { + flywayService.cleanDatabase(DEFAULT_DATASOURCE, new JdbcConnectionProperties(config, DEFAULT_DATASOURCE)); + for (String datasourceName : dataSourceNames) { + var jdbcConnectionProperties = new JdbcConnectionProperties(config, datasourceName); + List additionalMigrationLocations = getAdditionalFlywayMigrationLocations(datasourceName); + flywayService.migrateDatabase(datasourceName, jdbcConnectionProperties, additionalMigrationLocations); + } + } + + public List getAdditionalFlywayMigrationLocations(String datasourceName) { + // Only the default data has configurable additional migration scripts + if (!datasourceName.equals(DEFAULT_DATASOURCE)) { + return List.of(); + } + + String commaJoined = config.getString(EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS, ""); + return Arrays.stream(commaJoined.split(",")) + .map(String::trim) + .filter(it -> !it.isEmpty()) + .toList(); + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java new file mode 100644 index 000000000..6a7af5246 --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql.migration; + +import de.sovity.edc.extension.postgresql.connection.DriverManagerConnectionFactory; +import de.sovity.edc.extension.postgresql.connection.JdbcConnectionProperties; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.sql.datasource.ConnectionFactoryDataSource; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.output.MigrateResult; +import org.flywaydb.core.api.output.RepairResult; + +import java.util.ArrayList; +import java.util.List; +import javax.sql.DataSource; + +@RequiredArgsConstructor +public class FlywayService { + + private static final String MIGRATION_LOCATION_BASE = "classpath:migration"; + + private final Monitor monitor; + private final boolean tryRepairOnFailedMigration; + private final boolean cleanEnabled; + private final boolean clean; + + public void cleanDatabase(String datasourceName, JdbcConnectionProperties jdbcConnectionProperties) { + if (clean) { + monitor.info("Running flyway clean."); + var flyway = setupFlyway(datasourceName, jdbcConnectionProperties, List.of()); + flyway.clean(); + } + } + + public void migrateDatabase( + String datasourceName, + JdbcConnectionProperties jdbcConnectionProperties, + List additionalMigrationLocations + ) { + var flyway = setupFlyway(datasourceName, jdbcConnectionProperties, additionalMigrationLocations); + flyway.info().getInfoResult().migrations.stream() + .map(migration -> "Found migration: %s".formatted(migration.filepath)) + .forEach(monitor::info); + + try { + var migrateResult = flyway.migrate(); + handleFlywayMigrationResult(datasourceName, migrateResult); + } catch (FlywayException e) { + if (tryRepairOnFailedMigration) { + repairAndRetryMigration(datasourceName, flyway); + } else { + throw new EdcPersistenceException("Flyway migration failed for '%s'" + .formatted(datasourceName), e); + } + } + } + + private void repairAndRetryMigration(String datasourceName, Flyway flyway) { + try { + var repairResult = flyway.repair(); + handleFlywayRepairResult(datasourceName, repairResult); + var migrateResult = flyway.migrate(); + handleFlywayMigrationResult(datasourceName, migrateResult); + } catch (FlywayException e) { + throw new EdcPersistenceException("Flyway migration failed for '%s'" + .formatted(datasourceName), e); + } + } + + private void handleFlywayRepairResult(String datasourceName, RepairResult repairResult) { + if (!repairResult.repairActions.isEmpty()) { + var repairActions = String.join(", ", repairResult.repairActions); + monitor.info("Repair actions for datasource %s: %s" + .formatted(datasourceName, repairActions)); + } + + if (!repairResult.warnings.isEmpty()) { + var warnings = String.join(", ", repairResult.warnings); + throw new EdcPersistenceException("Repairing datasource %s failed: %s" + .formatted(datasourceName, warnings)); + } + } + + private Flyway setupFlyway( + String datasourceName, + JdbcConnectionProperties jdbcConnectionProperties, + List additionalMigrationLocations + ) { + var dataSource = getDataSource(jdbcConnectionProperties); + var migrationTableName = String.format("flyway_schema_history_%s", datasourceName); + var migrationLocations = new ArrayList(); + migrationLocations.add(String.join("/", MIGRATION_LOCATION_BASE, datasourceName)); + migrationLocations.addAll(additionalMigrationLocations); + monitor.info("Flyway migration locations for '%s': %s".formatted(datasourceName, migrationLocations)); + return Flyway.configure() + .baselineVersion(MigrationVersion.fromVersion("0.0.0")) + .baselineOnMigrate(true) + .failOnMissingLocations(true) + .dataSource(dataSource) + .table(migrationTableName) + .locations(migrationLocations.toArray(new String[0])) + .cleanDisabled(!cleanEnabled) + .load(); + } + + private DataSource getDataSource(JdbcConnectionProperties jdbcConnectionProperties) { + var connectionFactory = new DriverManagerConnectionFactory(jdbcConnectionProperties); + return new ConnectionFactoryDataSource(connectionFactory); + } + + private void handleFlywayMigrationResult(String datasourceName, MigrateResult migrateResult) { + if (migrateResult.migrationsExecuted > 0) { + monitor.info(String.format( + "Successfully migrated database for datasource %s " + + "from version %s to version %s", + datasourceName, + migrateResult.initialSchemaVersion, + migrateResult.targetSchemaVersion)); + } else { + monitor.info(String.format( + "No migration necessary for datasource %s. Current version is %s", + datasourceName, + migrateResult.initialSchemaVersion)); + } + } + + +} diff --git a/extensions/postgres-flyway/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/postgres-flyway/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..b25d94ffc --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.postgresql.PostgresFlywayExtension diff --git a/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_1__Asset_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_1__Asset_Schema.sql new file mode 100644 index 000000000..bd4b5da08 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_1__Asset_Schema.sql @@ -0,0 +1,52 @@ +-- +-- Copyright (c) 2022 Daimler TSS GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Daimler TSS GmbH - Initial SQL Query +-- + +-- THIS SCHEMA HAS BEEN WRITTEN AND TESTED ONLY FOR POSTGRES + +-- table: edc_asset +CREATE TABLE IF NOT EXISTS edc_asset +( + asset_id VARCHAR NOT NULL, + PRIMARY KEY (asset_id) +); + +-- table: edc_asset_dataaddress +CREATE TABLE IF NOT EXISTS edc_asset_dataaddress +( + asset_id_fk VARCHAR NOT NULL, + properties JSON NOT NULL, + PRIMARY KEY (asset_id_fk), + FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE +); +COMMENT ON COLUMN edc_asset_dataaddress.properties IS 'DataAddress properties serialized as JSON'; + +-- table: edc_asset_property +CREATE TABLE IF NOT EXISTS edc_asset_property +( + asset_id_fk VARCHAR NOT NULL, + property_name VARCHAR NOT NULL, + property_value VARCHAR NOT NULL, + property_type VARCHAR NOT NULL, + PRIMARY KEY (asset_id_fk, property_name), + FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE +); + +COMMENT ON COLUMN edc_asset_property.property_name IS + 'Asset property key'; +COMMENT ON COLUMN edc_asset_property.property_value IS + 'Asset property value'; +COMMENT ON COLUMN edc_asset_property.property_type IS + 'Asset property class name'; + +CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value + ON edc_asset_property (property_name, property_value); \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_2__Asset_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_2__Asset_Schema.sql new file mode 100644 index 000000000..410ef423a --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_2__Asset_Schema.sql @@ -0,0 +1,21 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_asset + ADD created_at BIGINT; + +UPDATE edc_asset SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_asset + ALTER COLUMN created_at SET NOT NULL; \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql new file mode 100644 index 000000000..80f09c506 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql @@ -0,0 +1,24 @@ +-- +-- Copyright (c) 2022 Daimler TSS GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Daimler TSS GmbH - Initial SQL Query +-- Microsoft Corporation - refactoring +-- + +-- table: edc_contract_definitions +-- only intended for and tested with H2 and Postgres! +CREATE TABLE IF NOT EXISTS edc_contract_definitions +( + contract_definition_id VARCHAR NOT NULL, + access_policy_id VARCHAR NOT NULL, + contract_policy_id VARCHAR NOT NULL, + selector_expression JSON NOT NULL, + PRIMARY KEY (contract_definition_id) +); diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql new file mode 100644 index 000000000..c1dd04041 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql @@ -0,0 +1,20 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_definitions + ADD created_at BIGINT; + +UPDATE edc_contract_definitions SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_contract_definitions + ALTER COLUMN created_at SET NOT NULL; \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql new file mode 100644 index 000000000..a1446460b --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql @@ -0,0 +1,14 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +ALTER TABLE edc_contract_definitions ADD validity BIGINT; \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql new file mode 100644 index 000000000..f166a9ce2 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql @@ -0,0 +1,14 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_contract_definitions SET validity=60*60*24*365 WHERE validity IS NULL; diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql new file mode 100644 index 000000000..82049c460 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql @@ -0,0 +1,78 @@ +-- Statements are designed for and tested with Postgres only! + +CREATE TABLE IF NOT EXISTS edc_lease +( + leased_by VARCHAR NOT NULL, + leased_at BIGINT, + lease_duration INTEGER DEFAULT 60000 NOT NULL, + lease_id VARCHAR NOT NULL + CONSTRAINT lease_pk + PRIMARY KEY +); + +COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; + +COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; + + +CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex + ON edc_lease (lease_id); + + + +CREATE TABLE IF NOT EXISTS edc_contract_agreement +( + agr_id VARCHAR NOT NULL + CONSTRAINT contract_agreement_pk + PRIMARY KEY, + provider_agent_id VARCHAR, + consumer_agent_id VARCHAR, + signing_date BIGINT, + start_date BIGINT, + end_date INTEGER, + asset_id VARCHAR NOT NULL, + policy JSON +); + + +CREATE TABLE IF NOT EXISTS edc_contract_negotiation +( + id VARCHAR NOT NULL + CONSTRAINT contract_negotiation_pk + PRIMARY KEY, + correlation_id VARCHAR, + counterparty_id VARCHAR NOT NULL, + counterparty_address VARCHAR NOT NULL, + protocol VARCHAR DEFAULT 'ids-multipart'::CHARACTER VARYING NOT NULL, + type INTEGER DEFAULT 0 NOT NULL, + state INTEGER DEFAULT 0 NOT NULL, + state_count INTEGER DEFAULT 0, + state_timestamp BIGINT, + error_detail VARCHAR, + agreement_id VARCHAR + CONSTRAINT contract_negotiation_contract_agreement_id_fk + REFERENCES edc_contract_agreement, + contract_offers JSON, + trace_context JSON, + lease_id VARCHAR + CONSTRAINT contract_negotiation_lease_lease_id_fk + REFERENCES edc_lease + ON DELETE SET NULL, + CONSTRAINT provider_correlation_id CHECK (type = '0' OR correlation_id IS NOT NULL) +); + +COMMENT ON COLUMN edc_contract_negotiation.agreement_id IS 'ContractAgreement serialized as JSON'; + +COMMENT ON COLUMN edc_contract_negotiation.contract_offers IS 'List serialized as JSON'; + +COMMENT ON COLUMN edc_contract_negotiation.trace_context IS 'Map serialized as JSON'; + + +CREATE INDEX IF NOT EXISTS contract_negotiation_correlationid_index + ON edc_contract_negotiation (correlation_id); + +CREATE UNIQUE INDEX IF NOT EXISTS contract_negotiation_id_uindex + ON edc_contract_negotiation (id); + +CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex + ON edc_contract_agreement (agr_id); \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql new file mode 100644 index 000000000..91587e425 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql @@ -0,0 +1,24 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_negotiation + ADD created_at BIGINT, + ADD updated_at BIGINT; + +UPDATE edc_contract_negotiation SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; +UPDATE edc_contract_negotiation SET updated_at=created_at; + +ALTER TABLE edc_contract_negotiation + ALTER COLUMN created_at SET NOT NULL; +ALTER TABLE edc_contract_negotiation + ALTER COLUMN updated_at SET NOT NULL; diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql b/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql new file mode 100644 index 000000000..243fda6be --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql @@ -0,0 +1,37 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +UPDATE edc_contract_negotiation +SET contract_offers = co.contract_offers_edited +FROM ( + SELECT + cn.id, + jsonb_agg( + jsonb_set( + jsonb_set( + elems, + '{contractStart}', + to_json(to_char(to_timestamp(created_at/1000) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ), + '{contractEnd}', + to_json(to_char(to_timestamp((created_at/1000) + 60 * 60 * 24 * 365) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ) + )::json as contract_offers_edited + FROM + edc_contract_negotiation cn, + jsonb_array_elements(cn.contract_offers::jsonb) elems + GROUP BY cn.id +) co +WHERE edc_contract_negotiation.id = co.id; + diff --git a/extensions/postgres-flyway/src/main/resources/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql new file mode 100644 index 000000000..b8329e9bd --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql @@ -0,0 +1,19 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - initial API and implementation for DataplaneInstances +-- +-- +CREATE TABLE IF NOT EXISTS edc_data_plane_instance +( + id VARCHAR NOT NULL, + data JSON NOT NULL, + PRIMARY KEY (id) +); diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V1_0_0_Example_Migration.sql b/extensions/postgres-flyway/src/main/resources/migration/default/V1_0_0_Example_Migration.sql new file mode 100644 index 000000000..efe29c442 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/default/V1_0_0_Example_Migration.sql @@ -0,0 +1,5 @@ +-- Empty example migration +-- This directory will contain all our Community Edition migrations in the future. +-- Commercial Edition migrations will have to be compatible version-wise. +-- This file is added to prevent flyway from crashing due no migrations. +SELECT 1; diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V2__Delete-Transfer-Processes-Trigger.sql b/extensions/postgres-flyway/src/main/resources/migration/default/V2__Delete-Transfer-Processes-Trigger.sql new file mode 100644 index 000000000..5ca84691b --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/default/V2__Delete-Transfer-Processes-Trigger.sql @@ -0,0 +1,41 @@ +-- Required for reasonably fast ON DELETE CASCADE from edc_transfer_process +create index data_request_transfer_process_id_idx + on edc_data_request (transfer_process_id); +-- Speed up sort + limit query +-- Include transferprocess_id to enable index-only scan +create index transfer_process_created_at_idx + on edc_transfer_process (created_at) include (transferprocess_id); + +-- Delete oldest row when table size exceeds 3000 rows +-- The row count should mostly stabilize slightly above 3000, as the reltuples data in pg_class is only updated by VACUUM +-- Unfortunately, I was not able to get conclusive results on the behavior under concurrent inserts +-- One problem is that the table might still grow over time, if concurrent inserts can delete the same row +-- To avoid this, we could delete two rows instead of just one +-- Then the table would shrink until the next auto-vacuum detects that it is below 3000 rows again +create function transfer_process_delete_old_rows() returns trigger as $$ +begin + delete from edc_transfer_process o + using ( + select i2.transferprocess_id + from edc_transfer_process i2 + order by i2.created_at + limit 2 + ) i, + ( + -- Hack to avoid count(*), which takes several hundred milliseconds + -- Not perfectly accurate, but close enough + -- Idea taken from: https://www.cybertec-postgresql.com/en/postgresql-count-made-fast/ + select pgc.reltuples::bigint as count + from pg_catalog.pg_class pgc + where pgc.relname = 'edc_transfer_process' + ) c + where i.transferprocess_id = o.transferprocess_id and c.count > 3000; + + return null; +end; +$$ language plpgsql; + +create trigger delete_old_rows after insert + on edc_transfer_process + for each row +execute function transfer_process_delete_old_rows(); \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V3__MS8-to-0.2.1.sql b/extensions/postgres-flyway/src/main/resources/migration/default/V3__MS8-to-0.2.1.sql new file mode 100644 index 000000000..e08d4659c --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/default/V3__MS8-to-0.2.1.sql @@ -0,0 +1,416 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables From MS8 to 0.1.0 EDC +-- +-- + +-- Migrates an Asset ID +create + or replace function pg_temp.migrate_asset_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:artifact:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Migrate a contract agreement ID to EDC 0 +create + or replace function pg_temp.migrate_contract_agreement_id(contract_agreement_id text, asset_id text) returns text as +$$ +begin + return pg_temp.base64encode(split_part(contract_agreement_id, ':', 1)) || ':' || + pg_temp.base64encode(asset_id) || ':' || + pg_temp.base64encode(split_part(contract_agreement_id, ':', 2)); +end; +$$ + language plpgsql; + +-- Migrates a Connector Endpoint to EDC 0 +create + or replace function pg_temp.migrate_connector_endpoint(endpoint text) returns text as +$$ +begin + return pg_temp.replace_suffix(endpoint, '/api/v1/ids/data', '/api/dsp'); +end; +$$ + language plpgsql; + +-- Migrates a Participant ID to EDC 0 +create + or replace function pg_temp.migrate_participant_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:connector:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Migrates an Asset Property Name to EDC 0 (if possible) +create + or replace function pg_temp.migrate_asset_property_name(asset_property_key text) returns text as +$$ +begin + return case asset_property_key + -- This list only contains properties that are directly mappable + -- Properties that require a new nested JSON structure are not included + when 'asset:prop:id' then 'https://w3id.org/edc/v0.0.1/ns/id' + when 'asset:prop:name' then 'http://purl.org/dc/terms/title' + when 'asset:prop:language' then 'http://purl.org/dc/terms/language' + when 'asset:prop:description' then 'http://purl.org/dc/terms/description' + when 'asset:prop:standardLicense' then 'http://purl.org/dc/terms/license' + when 'asset:prop:version' then 'http://www.w3.org/ns/dcat#version' + when 'asset:prop:keywords' then 'http://www.w3.org/ns/dcat#keyword' + when 'asset:prop:contenttype' then 'http://www.w3.org/ns/dcat#mediaType' + when 'asset:prop:endpointDocumentation' then 'http://www.w3.org/ns/dcat#landingPage' + when 'http://w3id.org/mds#dataCategory' then asset_property_key + when 'http://w3id.org/mds#dataSubcategory' then asset_property_key + when 'http://w3id.org/mds#dataModel' then asset_property_key + when 'http://w3id.org/mds#geoReferenceMethod' then asset_property_key + when 'http://w3id.org/mds#transportMode' then asset_property_key + when 'asset:prop:datasource:http:hints:proxyMethod' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod' + when 'asset:prop:datasource:http:hints:proxyPath' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath' + when 'asset:prop:datasource:http:hints:proxyQueryParams' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams' + when 'asset:prop:datasource:http:hints:proxyBody' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody' + else pg_temp.migrate_unknown_asset_property_name(asset_property_key) + end; +end; +$$ + language plpgsql; + +-- Migrates an unknown Asset Property Name to EDC 0 (if possible) +create + or replace function pg_temp.migrate_unknown_asset_property_name(asset_property_key text) returns text as +$$ +begin + asset_property_key := replace(asset_property_key, 'asset:prop:', ''); + if pg_temp.starts_with(asset_property_key, 'http://') or + pg_temp.starts_with(asset_property_key, 'https://') then + return asset_property_key; + end if; + + return 'http://unknown/' || replace(asset_property_key, ':', '-'); +end; +$$ + language plpgsql; + + +-- Migrates the "keywords" property value to EDC 0 +-- 'a, b'::text becomes '["a", "b"]'::text +create + or replace function pg_temp.migrate_asset_keywords(keywords_comma_joined text) returns text as +$$ +begin + return (select coalesce(json_agg(to_json(trim(keyword)))::text, '[]') + from unnest(regexp_split_to_array(keywords_comma_joined, ',')) as keyword + where trim(keyword) <> ''); +end; +$$ + language plpgsql; + +-- Migrates an asset property to EDC 0 +create + or replace function pg_temp.migrate_asset_property(property_name text, property_value text, property_type text) returns jsonb as +$$ +declare + name_mapped text; + value_mapped text; + type_mapped text; +begin + if property_name = 'asset:prop:id' then + name_mapped = pg_temp.migrate_asset_property_name(property_name); + value_mapped = pg_temp.migrate_asset_id(property_value); + type_mapped = property_type; + elsif property_name = 'asset:prop:keywords' then + name_mapped = pg_temp.migrate_asset_property_name(property_name); + value_mapped = pg_temp.migrate_asset_keywords(property_value); + type_mapped = 'java.util.ArrayList'; + elsif property_name = 'asset:prop:publisher' then + name_mapped = 'http://purl.org/dc/terms/publisher'; + value_mapped = jsonb_build_object('http://xmlns.com/foaf/0.1/homepage', + pg_temp.jsonld_value(property_value))::text; + type_mapped = 'java.util.LinkedHashMap'; + elsif property_name = 'asset:prop:curatorOrganizationName' or + property_name = 'asset:prop:originatorOrganization' then + name_mapped = 'http://purl.org/dc/terms/creator'; + value_mapped = jsonb_build_object('http://xmlns.com/foaf/0.1/name', + pg_temp.jsonld_value(property_value))::text; + type_mapped = 'java.util.LinkedHashMap'; + else + name_mapped = pg_temp.migrate_asset_property_name(property_name); + value_mapped = property_value; + type_mapped = property_type; + end if; + + return jsonb_build_object( + 'name', name_mapped, + 'value', value_mapped, + 'type', type_mapped + ); +end; +$$ + language plpgsql; + +-- Migrates a contract definition criterion operator +create + or replace function pg_temp.migrate_criterion_operator(op text) returns text as +$$ +begin + -- due to previous mixing of the criterion operator with ODRL operators, we need to ensure the data in the db + -- is correct + return case lower(op) + when 'eq' then '=' + when 'in' then 'in' + when 'like' then 'like' + else op + end; +end; +$$ + language plpgsql; + +-- Migrates a contract definition criterion to EDC 0 +create + or replace function pg_temp.migrate_criterion(criterion jsonb) returns jsonb as +$$ +declare + operand_left_mapped text; + operator_mapped text; + operand_right_mapped jsonb; +begin + operand_left_mapped = pg_temp.migrate_asset_property_name(criterion ->> 'operandLeft'); + operator_mapped = pg_temp.migrate_criterion_operator(criterion ->> 'operator'); + + if criterion ->> 'operandLeft' = 'asset:prop:id' then + if jsonb_typeof(criterion -> 'operandRight') = 'array' then + operand_right_mapped = (select jsonb_agg(to_jsonb(pg_temp.migrate_asset_id(items.item))) + from (select jsonb_array_elements_text(criterion -> 'operandRight') item) items); + else + operand_right_mapped = to_jsonb(pg_temp.migrate_asset_id(criterion ->> 'operandRight')); + end if; + else + operand_right_mapped = criterion -> 'operandRight'; + end if; + + return jsonb_build_object( + 'operandLeft', operand_left_mapped, + 'operator', operator_mapped, + 'operandRight', operand_right_mapped + ); +end; +$$ + language plpgsql; + +-- Migrates a contract offer JSON to EDC 0 +create + or replace function pg_temp.migrate_negotiation_contract_offer(contract_offer jsonb) returns jsonb as +$$ +begin + return (contract_offer - '{asset,provider,consumer,offerStart,offerEnd,contractStart,contractEnd}'::text[] + || jsonb_build_object('assetId', pg_temp.migrate_asset_id(contract_offer -> 'asset' ->> 'id'))); +end; +$$ + language plpgsql; + +-- Migrates a JSON array of contract offers to EDC 0 +create + or replace function pg_temp.migrate_negotiation_contract_offers(contract_offers jsonb) returns jsonb as +$$ +begin + return (select jsonb_agg(pg_temp.migrate_negotiation_contract_offer(contract_offers.contract_offer)) + from (select jsonb_array_elements(contract_offers) contract_offer) contract_offers); +end; +$$ + language plpgsql; + +-- Utility Function: Wraps a value in expanded JSON-LD +-- 'a'::text becomes '[{"@value": "a"}]'::jsonb +create + or replace function pg_temp.jsonld_value(value text) returns jsonb as +$$ +begin + return jsonb_build_array(jsonb_build_object('@value', to_jsonb(value))); +end; +$$ + language plpgsql; + +-- Utility Function: base64 encode +create + or replace function pg_temp.base64encode(str text) returns text as +$$ +begin + return encode(str::bytea, 'base64'); +end; +$$ + language plpgsql; + +-- Utility Function: replaceSuffix +create + or replace function pg_temp.replace_suffix(str text, old_suffix text, new_suffix text) returns text as +$$ +begin + return case + when pg_temp.ends_with(str, old_suffix) then + left(str, length(str) - length(old_suffix)) || new_suffix + else + str + end; +end; +$$ + language plpgsql; + +-- Utility Function: endsWith +create or replace function pg_temp.ends_with(str text, suffix text) + returns boolean as +$$ +begin + return right(str, length(suffix)) = suffix; +end; +$$ language plpgsql; + +-- Utility Function: startsWith +create or replace function pg_temp.starts_with(str text, prefix text) + returns boolean as +$$ +begin + return left(str, length(prefix)) = prefix; +end; +$$ language plpgsql; + +-- Asset IDs +alter table edc_asset_dataaddress + drop constraint edc_asset_dataaddress_asset_id_fk_fkey; +alter table edc_asset_property + drop constraint edc_asset_property_asset_id_fk_fkey; +update edc_asset +set asset_id = pg_temp.migrate_asset_id(asset_id); +update edc_asset_dataaddress +set asset_id_fk = pg_temp.migrate_asset_id(asset_id_fk); +update edc_asset_property +set asset_id_fk = pg_temp.migrate_asset_id(asset_id_fk); +update edc_contract_agreement +set asset_id = pg_temp.migrate_asset_id(asset_id); +update edc_data_request +set asset_id = pg_temp.migrate_asset_id(asset_id); +alter table edc_asset_dataaddress + add constraint edc_asset_dataaddress_asset_id_fk_fkey foreign key (asset_id_fk) references edc_asset (asset_id) on delete cascade; +alter table edc_asset_property + add constraint edc_asset_property_asset_id_fk_fkey foreign key (asset_id_fk) references edc_asset (asset_id) on delete cascade; + +-- Contract Agreement IDs +alter table edc_contract_negotiation + drop constraint contract_negotiation_contract_agreement_id_fk; +update edc_contract_negotiation +set agreement_id = pg_temp.migrate_contract_agreement_id(agreement_id, + pg_temp.migrate_asset_id(contract_offers -> 0 -> 'asset' ->> 'id')); +update edc_contract_agreement +set agr_id = pg_temp.migrate_contract_agreement_id(agr_id, asset_id); +update edc_data_request +set contract_id = pg_temp.migrate_contract_agreement_id(contract_id, asset_id); +alter table edc_contract_negotiation + add constraint contract_negotiation_contract_agreement_id_fk foreign key (agreement_id) references edc_contract_agreement (agr_id); + +-- Protocol +update edc_contract_negotiation +set protocol = 'dataspace-protocol-http'; + +-- Connector Endpoints +update edc_contract_negotiation +set counterparty_address = pg_temp.migrate_connector_endpoint(counterparty_address); +update edc_data_request +set connector_address = pg_temp.migrate_connector_endpoint(connector_address); + + +-- Participant IDs +update edc_data_request +set connector_id = pg_temp.migrate_participant_id(connector_id); +update edc_contract_agreement +set provider_agent_id = pg_temp.migrate_participant_id(provider_agent_id), + consumer_agent_id = pg_temp.migrate_participant_id(consumer_agent_id); + +-- Asset Properties +alter table edc_asset_property + add column property_is_private boolean; +delete +from edc_asset_property legacy_prop +where legacy_prop.property_name = 'asset:prop:originator'; +delete +from edc_asset_property legacy_prop +where legacy_prop.property_name = 'asset:prop:originatorOrganization' + and exists(select 1 + from edc_asset_property newer_prop + where legacy_prop.asset_id_fk = newer_prop.asset_id_fk + and newer_prop.property_name = 'asset:prop:curatorOrganizationName'); -- prevents errors from merging the legacy properties +update edc_asset_property +set property_name = pg_temp.migrate_asset_property(property_name, property_value, property_type) ->> 'name', + property_value = pg_temp.migrate_asset_property(property_name, property_value, property_type) ->> 'value', + property_type = pg_temp.migrate_asset_property(property_name, property_value, property_type) ->> 'type'; + +-- Contract Negotiation Type +alter table edc_contract_negotiation + drop constraint provider_correlation_id; +alter table edc_contract_negotiation + add column "type_new" text; +update edc_contract_negotiation +set "type_new" = case "type" when 1 then 'PROVIDER' else 'CONSUMER' end; +alter table edc_contract_negotiation + drop column "type"; +alter table edc_contract_negotiation + rename column "type_new" to "type"; +alter table edc_contract_negotiation + add constraint provider_correlation_id check (type = 'CONSUMER' OR correlation_id IS NOT NULL); + +-- Contract Negotiation Contract Offers +update edc_contract_negotiation +set contract_offers = pg_temp.migrate_negotiation_contract_offers(contract_offers::jsonb)::json; + +-- Contract Definitions Asset Selector +alter table edc_contract_definitions + rename column selector_expression to assets_selector; +with cd_updated as (select cd.contract_definition_id, + jsonb_agg(pg_temp.migrate_criterion(criterion))::json as asset_selector + from edc_contract_definitions cd, + jsonb_array_elements(cd.assets_selector::jsonb -> 'criteria') criterion + group by cd.contract_definition_id) +update edc_contract_definitions cd +set assets_selector = cd_updated.asset_selector +from cd_updated +where cd_updated.contract_definition_id = cd.contract_definition_id; + +-- Fix transfer processes stuck in running state +update edc_transfer_process +set state = 800 +where state = 600; + +-- Other DDL Changes +alter table edc_contract_negotiation + add column callback_addresses json; +alter table edc_contract_negotiation + add column pending boolean default false; +alter table edc_transfer_process + add column pending boolean default false; +alter table edc_transfer_process + add column callback_addresses json; + +alter table edc_transfer_process + rename column transferprocess_properties to private_properties; + +alter table edc_contract_definitions + drop column validity; +alter table edc_data_request + drop column if exists managed_resources; +alter table edc_data_request + drop column if exists properties; +alter table edc_data_request + drop column if exists transfer_type; diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V4__MS8-to-0.2.1_bugfixes.sql b/extensions/postgres-flyway/src/main/resources/migration/default/V4__MS8-to-0.2.1_bugfixes.sql new file mode 100644 index 000000000..ee64152ba --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/default/V4__MS8-to-0.2.1_bugfixes.sql @@ -0,0 +1,48 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables From MS8 to 0.1.0 EDC +-- +-- + +-- Migrates a Participant ID to EDC 0 +create + or replace function pg_temp.migrate_participant_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:connector:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Participant IDs +update edc_contract_negotiation +set counterparty_id = pg_temp.migrate_participant_id(counterparty_id); + +update edc_contract_agreement +set provider_agent_id = neg.counterparty_id +from edc_contract_negotiation neg +where neg.agreement_id = edc_contract_agreement.agr_id + and neg.type = 'CONSUMER'; + +update edc_contract_agreement +set consumer_agent_id = neg.counterparty_id +from edc_contract_negotiation neg +where neg.agreement_id = edc_contract_agreement.agr_id + and neg.type = 'PROVIDER'; + +-- Optimizations for Transfer Processes +create index transfer_process_status + on edc_transfer_process (state); + +-- Fix transfer processes stuck in running state +update edc_transfer_process +set state = 800 +where state = 600; diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V5__Mobility_DCAT_Mapping.sql b/extensions/postgres-flyway/src/main/resources/migration/default/V5__Mobility_DCAT_Mapping.sql new file mode 100644 index 000000000..64a9ed66c --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/default/V5__Mobility_DCAT_Mapping.sql @@ -0,0 +1,76 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - SQL Script +-- +-- + + +-- "http://www.w3.org/ns/dcat#distribution": { +-- "http://www.w3.org/ns/dcat#mediaType": "previously http://www.w3.org/ns/dcat#mediaType", +-- "https://w3id.org/mobilitydcat-ap/mobilityDataStandard": { +-- "@id": "previously http://w3id.org/mds#dataModel" +-- } +-- }, +with data as ( + select asset_id, + (select property_value from edc_asset_property p where p.property_name = 'http://www.w3.org/ns/dcat#mediaType' and p.asset_id_fk = a.asset_id) as media_type, + (select property_value from edc_asset_property p where p.property_name = 'http://w3id.org/mds#dataModel' and p.asset_id_fk = a.asset_id) as data_model + from edc_asset a +) +insert into edc_asset_property (asset_id_fk, property_name, property_type, property_value) ( + select asset_id, + 'http://www.w3.org/ns/dcat#distribution', + 'java.util.LinkedHashMap', + jsonb_build_object( + 'http://www.w3.org/ns/dcat#mediaType', media_type, + 'https://w3id.org/mobilitydcat-ap/mobilityDataStandard', jsonb_build_object('@id', data_model) + )::text + from data + where media_type is not null or data_model is not null +); +delete from edc_asset_property where property_name = 'http://www.w3.org/ns/dcat#mediaType'; +delete from edc_asset_property where property_name = 'http://w3id.org/mds#dataModel'; + + +-- "https://w3id.org/mobilitydcat-ap/mobilityTheme": { +-- "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category": "previously http://w3id.org/mds#dataCategory", +-- "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category": "previously http://w3id.org/mds#dataSubcategory" +-- }, +with data as ( + select asset_id, + (select property_value from edc_asset_property p where p.property_name = 'http://w3id.org/mds#dataCategory' and p.asset_id_fk = a.asset_id) as data_category, + (select property_value from edc_asset_property p where p.property_name = 'http://w3id.org/mds#dataSubcategory' and p.asset_id_fk = a.asset_id) as data_subcategory + from edc_asset a +) +insert into edc_asset_property (asset_id_fk, property_name, property_type, property_value) ( + select asset_id, + 'https://w3id.org/mobilitydcat-ap/mobilityTheme', + 'java.util.LinkedHashMap', + jsonb_build_object( + 'https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category', data_category, + 'https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category', data_subcategory + )::text + from data + where data_category is not null or data_subcategory is not null +); +delete from edc_asset_property where property_name = 'http://w3id.org/mds#dataCategory'; +delete from edc_asset_property where property_name = 'http://w3id.org/mds#dataSubcategory'; + + +-- "https://w3id.org/mobilitydcat-ap/georeferencingMethod": "previously http://w3id.org/mds#geoReferenceMethod", +update edc_asset_property +set property_name = 'https://w3id.org/mobilitydcat-ap/georeferencingMethod' +where property_name = 'http://w3id.org/mds#geoReferenceMethod'; + +-- "https://w3id.org/mobilitydcat-ap/transportMode": "previously http://w3id.org/mds#transportMode" +update edc_asset_property +set property_name = 'https://w3id.org/mobilitydcat-ap/transportMode' +where property_name = 'http://w3id.org/mds#transportMode'; diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V6__Fix_DataModel_ID_Field.sql b/extensions/postgres-flyway/src/main/resources/migration/default/V6__Fix_DataModel_ID_Field.sql new file mode 100644 index 000000000..f8b0f7f98 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/default/V6__Fix_DataModel_ID_Field.sql @@ -0,0 +1,63 @@ +-- +-- Copyright (c) 2024 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - SQL Script +-- +-- + +create + or replace function pg_temp.migrate_distribution(distribution jsonb) returns jsonb as +$$ +declare + data_standard jsonb; + data_standard_path text[]; +begin + data_standard_path := '{https://w3id.org/mobilitydcat-ap/mobilityDataStandard}'; + data_standard := distribution #> data_standard_path; + + if jsonb_typeof(data_standard) = 'object' then + data_standard := pg_temp.migrate_mobility_data_standard(data_standard); + elsif jsonb_typeof(data_standard) = 'array' then + data_standard := (select jsonb_agg(pg_temp.migrate_mobility_data_standard(it)) + from jsonb_array_elements(data_standard) as it); + end if; + + return jsonb_set(distribution, data_standard_path, data_standard, true); +end; +$$ language plpgsql; + + +create + or replace function pg_temp.migrate_mobility_data_standard(data_standard jsonb) returns jsonb as +$$ +begin + return pg_temp.remove_if_blank(data_standard, '{@id}'); +end; +$$ language plpgsql; + + +create + or replace function pg_temp.remove_if_blank(obj jsonb, path text[]) returns jsonb as +$$ +declare + value text; +begin + value := obj #>> path; + if value is null or trim(value) = '' then + obj := obj #- path; + end if; + return obj; +end; +$$ language plpgsql; + + +update edc_asset_property +set property_value = pg_temp.migrate_distribution(property_value::jsonb)::text +where property_name = 'http://www.w3.org/ns/dcat#distribution'; diff --git a/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_1__Policy_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_1__Policy_Schema.sql new file mode 100644 index 000000000..87199a519 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_1__Policy_Schema.sql @@ -0,0 +1,39 @@ +-- +-- Copyright (c) 2022 ZF Friedrichshafen AG +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- ZF Friedrichshafen AG - Initial SQL Query +-- + +-- Statements are designed for and tested with Postgres only! + +-- table: edc_policydefinitions +CREATE TABLE IF NOT EXISTS edc_policydefinitions +( + policy_id VARCHAR NOT NULL, + permissions JSON, + prohibitions JSON, + duties JSON, + extensible_properties JSON, + inherits_from VARCHAR, + assigner VARCHAR, + assignee VARCHAR, + target VARCHAR, + policy_type VARCHAR NOT NULL, + PRIMARY KEY (policy_id) +); + +COMMENT ON COLUMN edc_policydefinitions.permissions IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.prohibitions IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.duties IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.extensible_properties IS 'Java Map serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.policy_type IS 'Java PolicyType serialized as JSON'; + +CREATE UNIQUE INDEX IF NOT EXISTS edc_policydefinitions_id_uindex + ON edc_policydefinitions (policy_id); diff --git a/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_2__Policy_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_2__Policy_Schema.sql new file mode 100644 index 000000000..e6dcb8266 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_2__Policy_Schema.sql @@ -0,0 +1,20 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_policydefinitions + ADD created_at BIGINT; + +UPDATE edc_policydefinitions SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_policydefinitions + ALTER COLUMN created_at SET NOT NULL; diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql new file mode 100644 index 000000000..a5764deaf --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql @@ -0,0 +1,86 @@ +-- Statements are designed for and tested with Postgres only! + +CREATE TABLE IF NOT EXISTS edc_lease +( + leased_by VARCHAR NOT NULL, + leased_at BIGINT, + lease_duration INTEGER NOT NULL, + lease_id VARCHAR NOT NULL + CONSTRAINT lease_pk + PRIMARY KEY +); + +COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; + +COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; + +CREATE TABLE IF NOT EXISTS edc_transfer_process +( + transferprocess_id VARCHAR NOT NULL + CONSTRAINT transfer_process_pk + PRIMARY KEY, + type VARCHAR NOT NULL, + state INTEGER NOT NULL, + state_count INTEGER DEFAULT 0 NOT NULL, + state_time_stamp BIGINT, + created_time_stamp BIGINT, + trace_context JSON, + error_detail VARCHAR, + resource_manifest JSON, + provisioned_resource_set JSON, + content_data_address JSON, + deprovisioned_resources JSON, + lease_id VARCHAR + CONSTRAINT transfer_process_lease_lease_id_fk + REFERENCES edc_lease + ON DELETE SET NULL +); + +COMMENT ON COLUMN edc_transfer_process.trace_context IS 'Java Map serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.resource_manifest IS 'java ResourceManifest serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.provisioned_resource_set IS 'ProvisionedResourceSet serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.content_data_address IS 'DataAddress serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.deprovisioned_resources IS 'List of deprovisioned resources, serialized as JSON'; + + +CREATE UNIQUE INDEX IF NOT EXISTS transfer_process_id_uindex + ON edc_transfer_process (transferprocess_id); + +CREATE TABLE IF NOT EXISTS edc_data_request +( + datarequest_id VARCHAR NOT NULL + CONSTRAINT data_request_pk + PRIMARY KEY, + process_id VARCHAR NOT NULL, + connector_address VARCHAR NOT NULL, + protocol VARCHAR NOT NULL, + connector_id VARCHAR, + asset_id VARCHAR NOT NULL, + contract_id VARCHAR NOT NULL, + data_destination JSON NOT NULL, + managed_resources BOOLEAN DEFAULT TRUE, + properties JSON, + transfer_type JSON, + transfer_process_id VARCHAR NOT NULL + CONSTRAINT data_request_transfer_process_id_fk + REFERENCES edc_transfer_process + ON UPDATE RESTRICT ON DELETE CASCADE +); + +COMMENT ON COLUMN edc_data_request.data_destination IS 'DataAddress serialized as JSON'; + +COMMENT ON COLUMN edc_data_request.properties IS 'java Map serialized as JSON'; + +COMMENT ON COLUMN edc_data_request.transfer_type IS 'TransferType serialized as JSON'; + + +CREATE UNIQUE INDEX IF NOT EXISTS data_request_id_uindex + ON edc_data_request (datarequest_id); + +CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex + ON edc_lease (lease_id); + diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql new file mode 100644 index 000000000..db875a04c --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql @@ -0,0 +1,28 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_transfer_process + RENAME COLUMN created_time_stamp TO created_at; + +UPDATE edc_transfer_process + SET created_at = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 + WHERE created_at = NULL; + +ALTER TABLE edc_transfer_process + ADD updated_at BIGINT; + +UPDATE edc_transfer_process SET updated_at=created_at; + +ALTER TABLE edc_transfer_process ALTER COLUMN updated_at SET NOT NULL; +ALTER TABLE edc_transfer_process ALTER COLUMN created_at SET NOT NULL; diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql new file mode 100644 index 000000000..cd2d45af7 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql @@ -0,0 +1,14 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +ALTER TABLE edc_transfer_process ADD transferprocess_properties JSON; diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_4__Set_Default_Properties.sql b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_4__Set_Default_Properties.sql new file mode 100644 index 000000000..41402c7f6 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_4__Set_Default_Properties.sql @@ -0,0 +1,14 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_transfer_process SET transferprocess_properties = '{}'::json WHERE transferprocess_properties IS NULL; diff --git a/extensions/sovity-edc-extensions-package/README.md b/extensions/sovity-edc-extensions-package/README.md new file mode 100644 index 000000000..ed0986757 --- /dev/null +++ b/extensions/sovity-edc-extensions-package/README.md @@ -0,0 +1,33 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension Package:
      sovity's Core EDC Extensions

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension Package + +This extension packages common [sovity Community Edition EDC Extensions](..) used in all sovity EDC Connectors. +See [build.gradle.kts](build.gradle.kts) for a list of contained extensions. + +## Why does this extension exist? + +The extension allows to add a new policy and have it rolled out across all editions managed and +maintained by sovity. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/sovity-edc-extensions-package/build.gradle.kts b/extensions/sovity-edc-extensions-package/build.gradle.kts new file mode 100644 index 000000000..69af1e1ee --- /dev/null +++ b/extensions/sovity-edc-extensions-package/build.gradle.kts @@ -0,0 +1,31 @@ +val edcVersion: String by project +val edcGroup: String by project +val restAssured: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + // Policies + api(project(":extensions:policy-referring-connector")) + api(project(":extensions:policy-time-interval")) + api(project(":extensions:policy-always-true")) + + // API Extensions + api(project(":extensions:edc-ui-config")) + api(project(":extensions:last-commit-info")) + api(project(":extensions:wrapper:wrapper")) +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/test-backend-controller/README.md b/extensions/test-backend-controller/README.md new file mode 100644 index 000000000..12d5e1e5a --- /dev/null +++ b/extensions/test-backend-controller/README.md @@ -0,0 +1,29 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Test Backend

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension + +An EDC extension that adds a dummy data source and a dummy data sink on the Web Endpoint (usually :11001). + +## Why does this extension exist? + +This allows us to emulate a data address for our E2E tests. + +## License +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact +sovity GmbH - contact@sovity.de diff --git a/extensions/test-backend-controller/build.gradle.kts b/extensions/test-backend-controller/build.gradle.kts new file mode 100644 index 000000000..56ffc60c7 --- /dev/null +++ b/extensions/test-backend-controller/build.gradle.kts @@ -0,0 +1,23 @@ +val edcVersion: String by project +val edcGroup: String by project + +plugins { + `java-library` +} + +dependencies { + api("${edcGroup}:api-core:${edcVersion}") + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:http:${edcVersion}") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java new file mode 100644 index 000000000..3f7777cd0 --- /dev/null +++ b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.testbackendcontroller; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +import java.util.concurrent.ConcurrentHashMap; + +@Path("/test-backend") +public class TestBackendController { + + private final ConcurrentHashMap dataSink = new ConcurrentHashMap<>(); + + @GET + @Path("/data-sink/spy") + @Produces(MediaType.APPLICATION_JSON) + public String getDataSinkValue() { + return getDataSinkValue("default"); + } + + @PUT + @Path("/data-sink") + public void setDataSinkValue(String incomingData) { + setDataSinkValue("default", incomingData); + } + + @GET + @Path("/data-source") + @Produces(MediaType.APPLICATION_JSON) + public String echoForDataSource(@QueryParam("data") String message) { + return message; + } + + @GET + @Path("/data-source/echo-query-params") + @Produces(MediaType.APPLICATION_JSON) + public String echoDataSourceQueryParams(@Context UriInfo uriInfo) { + return uriInfo.getRequestUri().getQuery(); + } + + @GET + @Path("/{testId}/data-sink/spy") + @Produces(MediaType.APPLICATION_JSON) + public String getDataSinkValue(@PathParam("testId") String testId) { + return dataSink.getOrDefault(testId, ""); + } + + @PUT + @Path("/{testId}/data-sink") + public void setDataSinkValue(@PathParam("testId") String testId, String incomingData) { + dataSink.put(testId, incomingData); + } +} diff --git a/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java new file mode 100644 index 000000000..4d5943187 --- /dev/null +++ b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.testbackendcontroller; + +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +public class TestBackendExtension implements ServiceExtension { + @Inject + private WebService webService; + + @Override + public String name() { + return "Test Backend Controller"; + } + + @Override + public void initialize(ServiceExtensionContext context) { + webService.registerResource(new TestBackendController()); + } +} diff --git a/extensions/test-backend-controller/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/test-backend-controller/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..e163dd550 --- /dev/null +++ b/extensions/test-backend-controller/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.testbackendcontroller.TestBackendExtension diff --git a/extensions/transfer-process-status-checker/README.md b/extensions/transfer-process-status-checker/README.md new file mode 100644 index 000000000..ab773f763 --- /dev/null +++ b/extensions/transfer-process-status-checker/README.md @@ -0,0 +1,31 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Transfer Process Status Checker

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension + +Bugfix extension for `Eclipse EDC [0.2.1, 0.3)`, marks transfer processes as `COMPLETED`. + +## Why does this extension exist? + +We cannot directly upgrade to `Eclipse EDC >=0.3` now, but will of course do so soon. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/transfer-process-status-checker/build.gradle.kts b/extensions/transfer-process-status-checker/build.gradle.kts new file mode 100644 index 000000000..cf7424b81 --- /dev/null +++ b/extensions/transfer-process-status-checker/build.gradle.kts @@ -0,0 +1,23 @@ +val edcVersion: String by project +val edcGroup: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api("${edcGroup}:transfer-spi:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/transfer-process-status-checker/src/main/java/de/sovity/edc/extension/transfer/TransferProcessStatusCheckerExtension.java b/extensions/transfer-process-status-checker/src/main/java/de/sovity/edc/extension/transfer/TransferProcessStatusCheckerExtension.java new file mode 100644 index 000000000..a69def634 --- /dev/null +++ b/extensions/transfer-process-status-checker/src/main/java/de/sovity/edc/extension/transfer/TransferProcessStatusCheckerExtension.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.transfer; + +import org.eclipse.edc.connector.transfer.spi.status.StatusCheckerRegistry; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.util.List; + +public class TransferProcessStatusCheckerExtension implements ServiceExtension { + private static final String EXTENSION_NAME = "Transfer Process Status Checker"; + + @Inject + private StatusCheckerRegistry statusCheckerRegistry; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + List.of("HttpProxy", "HttpData").forEach(this::registerStatusChecker); + } + + private void registerStatusChecker(String transferType) { + statusCheckerRegistry.register(transferType, (transferProcess, resources) -> true); + } +} diff --git a/extensions/transfer-process-status-checker/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/transfer-process-status-checker/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..0183251a9 --- /dev/null +++ b/extensions/transfer-process-status-checker/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.transfer.TransferProcessStatusCheckerExtension diff --git a/extensions/wrapper/README.md b/extensions/wrapper/README.md new file mode 100644 index 000000000..85c01afdd --- /dev/null +++ b/extensions/wrapper/README.md @@ -0,0 +1,53 @@ + +
      +
      + + Logo + + +

      sovity EDC API Wrapper

      + +

      + Report Bug + · + Request Feature +

      +
      + +## sovity EDC API Wrapper + +We provide a full type-safe and opinionated API Wrapper for better access to the EDC Connector's functionality. + +## Explore + +Create and consume Data Offers using clean type-safe JSON REST APIs: +- [API Wrapper OpenAPI YAML](../../docs/sovity-edc-api-wrapper.yaml). +- [Java API Client Library](./clients/java-client) +- [TypeScript API Client Library](./clients/typescript-client) + +## Compatibility + +Our EDC API Wrapper APIs and API Clients are compatible with both our sovity EDC Community Edition and sovity EDC Enterprise Editions. + +## Modules + +- The [sovity EDC API Wrapper Extension](./wrapper), serving implementations for our Community Edition APIs. +- API Definitions: + - The sovity Community Edition EDC [API Definitions](./wrapper-api), including the [Connector UI API](wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui) and [Use Case API](wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase). + - The sovity Enterprise Edition EDC [API Definitions](./wrapper-ee-api). +- [Client Libraries](./clients) and example projects: + - [Java API Client Library](./clients/java-client) + - [Java API Client Library Example](./clients/java-client-example) + - [TypeScript API Client Library](./clients/typescript-client) + - [TypeScript API Client Library Example](./clients/typescript-client-example) +- Utilities: + - Broker UI / Connector UI [Common Models](./wrapper-common-api) + - Broker / Connector [Common Services](./wrapper-common-mappers) + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/clients/java-client-example/.gitignore b/extensions/wrapper/clients/java-client-example/.gitignore new file mode 100644 index 000000000..285b6baee --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/.gitignore @@ -0,0 +1,36 @@ +# Gradle +.gradle/ +build/ + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/extensions/wrapper/clients/java-client-example/README.md b/extensions/wrapper/clients/java-client-example/README.md new file mode 100644 index 000000000..57cdfd6fe --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/README.md @@ -0,0 +1,30 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Quarkus Example Project

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +Example Quarkus Application that uses the Java API Client Library. + +A full example providing and consuming a data offer using the API Wrapper Client Library can be found +in [ApiWrapperDemoTest.java](../../../../tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java). + +## License + +Apache License 2.0 - see [LICENSE](../../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/clients/java-client-example/build.gradle.kts b/extensions/wrapper/clients/java-client-example/build.gradle.kts new file mode 100644 index 000000000..f43ea26ca --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + java + id("io.quarkus") version "2.16.6.Final" +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId = "io.quarkus.platform" +val quarkusPlatformArtifactId = "quarkus-bom" +val quarkusPlatformVersion = "2.16.6.Final" + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive-jackson") + + implementation(project(":extensions:wrapper:clients:java-client")) + + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.quarkus:quarkus-junit5-mockito") + testImplementation("io.rest-assured:rest-assured") +} + +group = "de.sovity.edc.client.examples" + +tasks.withType { + systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager") +} + +tasks.withType { + options.compilerArgs.add("-parameters") +} + +checkstyle { + // Gradle is unhappy with checkstyle accessing quarkus sources causing lots of warnings + this.sourceSets = emptyList() +} diff --git a/extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/EdcClientSetup.java b/extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/EdcClientSetup.java new file mode 100644 index 000000000..bd720f00e --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/EdcClientSetup.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.examples; + +import de.sovity.edc.client.EdcClient; +import io.quarkus.logging.Log; +import io.quarkus.runtime.configuration.ConfigUtils; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; + +public class EdcClientSetup { + + @ConfigProperty(name = "my-edc.management-api-url") + String managementApiUrl; + + @ConfigProperty(name = "my-edc.management-api-key") + String managementApiKey; + + @Produces + @ApplicationScoped + public EdcClient buildEdcClient() { + var client = EdcClient.builder() + .managementApiUrl(managementApiUrl) + .managementApiKey(managementApiKey) + .build(); + testEdcConnection(client); + return client; + } + + private void testEdcConnection(EdcClient client) { + if (ConfigUtils.getProfiles().contains("test")) { + Log.info("Skipping EDC connection test."); + return; + } + + client.testConnection(); + Log.info("Successfully connected to EDC Connector %s.".formatted(managementApiUrl)); + } +} diff --git a/extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/GreetingResource.java b/extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/GreetingResource.java new file mode 100644 index 000000000..c03f33f8d --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/src/main/java/de/sovity/edc/client/examples/GreetingResource.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.examples; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.KpiResult; + +import java.util.List; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/") +public class GreetingResource { + @Inject + EdcClient edcClient; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public KpiResult getKpis() { + return edcClient.useCaseApi().getKpis(); + } + + @GET + @Path("supported-policy-functions") + @Produces(MediaType.APPLICATION_JSON) + public List getSupportedPolicyFunctions() { + return edcClient.useCaseApi().getSupportedFunctions(); + } +} diff --git a/extensions/wrapper/clients/java-client-example/src/main/resources/application.properties b/extensions/wrapper/clients/java-client-example/src/main/resources/application.properties new file mode 100644 index 000000000..642af650c --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/src/main/resources/application.properties @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 sovity GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# sovity GmbH - init +# + +my-edc.management-api-url=http://localhost:11002/api/management +my-edc.management-api-key=ApiKeyDefaultValue diff --git a/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java b/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java new file mode 100644 index 000000000..4617344e5 --- /dev/null +++ b/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.examples; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.api.UseCaseApi; +import de.sovity.edc.client.gen.model.KpiResult; +import de.sovity.edc.client.gen.model.TransferProcessStatesDto; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@QuarkusTest +class GreetingResourceTest { + @InjectMock + EdcClient edcClient; + + UseCaseApi useCaseApi; + + @BeforeEach + void setup() { + useCaseApi = mock(UseCaseApi.class); + when(edcClient.useCaseApi()).thenReturn(useCaseApi); + } + + @Test + void testGetKpis() { + var kpiResult = new KpiResult(); + kpiResult.setAssetsCount(3); + when(useCaseApi.getKpis()).thenReturn(kpiResult); + + given() + .when().get("/") + .then() + .statusCode(200) + .body("assetsCount", is(3)); + } + +} diff --git a/extensions/wrapper/clients/java-client/README.md b/extensions/wrapper/clients/java-client/README.md new file mode 100644 index 000000000..fc1c6e264 --- /dev/null +++ b/extensions/wrapper/clients/java-client/README.md @@ -0,0 +1,112 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Java API Client

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +Java API Client Library to be imported and used in arbitrary applications like use-case backends. + +An example project using this client can be found [here](../java-client-example). + +## Installation + +```xml + + + de.sovity.edc + client + ${sovity-edc-extensions.version} + +``` + +## Usage + +### Example Consuming and Providing a Data Offer + +A full example providing and consuming a data offer using the API Wrapper Client Library can be found +in [ApiWrapperDemoTest.java](../../../../tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java). + +### Example Using API Key Auth + +```java +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.KpiResult; + +/** + * Example using a sovity Community Edition EDC Connector + */ +public class WrapperClientExample { + + public static final String CONNECTOR_ENDPOINT = "http://localhost:11002/api/management/v2"; + public static final String CONNECTOR_API_KEY = "..."; + + public static void main(String[] args) { + // Configure Client + EdcClient client = EdcClient.builder() + .managementApiUrl(CONNECTOR_ENDPOINT) + .managementApiKey(CONNECTOR_API_KEY) + .build(); + + // EDC API Wrapper APIs are now available for use + KpiResult kpiResult = client.useCaseApi().getKpis(); + System.out.println(kpiResult); + } +} + +``` + +### Example Using OAuth2 Client Credentials + +```java +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.KpiResult; +import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; +import de.sovity.edc.client.oauth2.SovityKeycloakUrl; + +/** + * Example using a productive Connector-as-a-Service (CaaS) EDC Connector + */ +public class WrapperClientExample { + + public static final String CONNECTOR_ENDPOINT = + "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/data"; + public static final String CLIENT_ID = "{{your-connector}}-app"; + public static final String CLIENT_SECRET = "..."; + + public static void main(String[] args) { + // Configure Client + EdcClient client = EdcClient.builder() + .managementApiUrl(CONNECTOR_ENDPOINT) + .oauth2ClientCredentials(OAuth2ClientCredentials.builder() + .tokenUrl(SovityKeycloakUrl.PRODUCTION) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .build()) + .build(); + + // EDC API Wrapper APIs are now available for use + KpiResult kpiResult = client.useCaseApi().getKpis(); + System.out.println(kpiResult); + } +} +``` + +## License + +Apache License 2.0 - see [LICENSE](../../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/clients/java-client/build.gradle.kts b/extensions/wrapper/clients/java-client/build.gradle.kts new file mode 100644 index 000000000..844184df4 --- /dev/null +++ b/extensions/wrapper/clients/java-client/build.gradle.kts @@ -0,0 +1,129 @@ +val edcVersion: String by project +val edcGroup: String by project +val restAssured: String by project +val assertj: String by project +val mockitoVersion: String by project +val lombokVersion: String by project +val jettyVersion: String by project +val jettyGroup: String by project + + +plugins { + `java-library` + `maven-publish` + id("org.openapi.generator") version "6.6.0" +} + +repositories { + mavenCentral() +} + +// By using a separate configuration we can skip having the Extension Jar in our runtime classpath +val openapiYaml = configurations.create("openapiGenerator") + +dependencies { + // We only need the openapi.yaml file from this dependency + openapiYaml(project(":extensions:wrapper:wrapper-api")) { + isTransitive = false + } + + // Generated Client's Dependencies + implementation("io.swagger:swagger-annotations:1.6.11") + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("com.google.code.gson:gson:2.10.1") + implementation("io.gsonfire:gson-fire:1.8.5") + implementation("org.openapitools:jackson-databind-nullable:0.2.6") + implementation("org.apache.commons:commons-lang3:3.13.0") + implementation("jakarta.annotation:jakarta.annotation-api:1.3.5") + + // Lombok + compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +// Extract the openapi file from the JAR +val openapiFile = "sovity-edc-api-wrapper.yaml" +task("extractOpenapiYaml") { + dependsOn(openapiYaml) + into("${project.buildDir}") + from(zipTree(openapiYaml.singleFile)) { + include(openapiFile) + } +} + +tasks.getByName("openApiGenerate") { + dependsOn("extractOpenapiYaml") + generatorName.set("java") + configOptions.set(mutableMapOf( + "invokerPackage" to "de.sovity.edc.client.gen", + "apiPackage" to "de.sovity.edc.client.gen.api", + "modelPackage" to "de.sovity.edc.client.gen.model", + "caseInsensitiveResponseHeaders" to "true", + "additionalModelTypeAnnotations" to "@lombok.AllArgsConstructor\n@lombok.Builder", + "annotationLibrary" to "swagger1", + "hideGenerationTimestamp" to "true", + "useRuntimeException" to "true", + )) + + inputSpec.set("${project.buildDir}/${openapiFile}") + outputDir.set("${project.buildDir}/generated/client-project") +} + +task("postprocessGeneratedClient") { + dependsOn("openApiGenerate") + from("${project.buildDir}/generated/client-project/src/main/java") + + // @lombok.Builder clashes with the following generated model file. + // It is the base class for OAS3 polymorphism via allOf/anyOf, which we won't use anyway. + exclude("**/AbstractOpenApiSchema.java") + + // The Jax-RS dependency suggested by the generated project was causing issues with quarkus. + // It was again only required for the polymorphism, which we won't use anyway. + filter { if (it == "import javax.ws.rs.core.GenericType;") "" else it } + + into("${project.buildDir}/generated/sources/openapi/java/main") +} +sourceSets["main"].java.srcDir("${project.buildDir}/generated/sources/openapi/java/main") + +checkstyle { + // Checkstyle loathes the generated files + // TODO make checkstyle skip generated files only + this.sourceSets = emptyList() +} + + +tasks.getByName("compileJava") { + dependsOn("postprocessGeneratedClient") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + withSourcesJar() + withJavadocJar() +} + +tasks.withType { + val fullOptions = this.options as StandardJavadocDocletOptions + fullOptions.tags = listOf("http.response.details:a:Http Response Details") + fullOptions.addStringOption("Xdoclint:none", "-quiet") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + + +publishing { + publications { + create(project.name) { + artifactId = "client" + from(components["java"]) + } + } +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClient.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClient.java new file mode 100644 index 000000000..9f81cc8a1 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClient.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import de.sovity.edc.client.gen.api.EnterpriseEditionApi; +import de.sovity.edc.client.gen.api.UiApi; +import de.sovity.edc.client.gen.api.UseCaseApi; +import lombok.Value; +import lombok.experimental.Accessors; + +/** + * API Client for our EDC API Wrapper. + */ +@Value +@Accessors(fluent = true) +public class EdcClient { + UiApi uiApi; + UseCaseApi useCaseApi; + EnterpriseEditionApi enterpriseEditionApi; + + public static EdcClientBuilder builder() { + return new EdcClientBuilder(); + } + + public void testConnection() { + useCaseApi.getKpis(); + } +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java new file mode 100644 index 000000000..64a0647b1 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientBuilder.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import de.sovity.edc.client.gen.ApiClient; +import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import okhttp3.OkHttpClient; + +import java.util.function.Consumer; + +@Getter +@Setter +@Accessors(fluent = true, chain = true) +public class EdcClientBuilder { + /** + * Management API Base URL, e.g. https://my-connector.com/control/management + */ + private String managementApiUrl; + + /** + * Enables EDC Management API Key authentication. + */ + private String managementApiKey = "ApiKeyDefaultValue"; + + /** + * Enables OAuth2 "Client Credentials Flow" authentication. + */ + private OAuth2ClientCredentials oauth2ClientCredentials; + + /** + * Custom configurer for the {@link ApiClient} and the {@link ApiClient#getHttpClient()}/{@link ApiClient#setHttpClient(OkHttpClient)} + * for environments with custom authentication mechanisms. + */ + private Consumer customConfigurer; + + + public EdcClient build() { + return EdcClientFactory.newClient(this); + } +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientFactory.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientFactory.java new file mode 100644 index 000000000..02ad27da4 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/EdcClientFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client; + +import de.sovity.edc.client.gen.ApiClient; +import de.sovity.edc.client.gen.api.EnterpriseEditionApi; +import de.sovity.edc.client.gen.api.UiApi; +import de.sovity.edc.client.gen.api.UseCaseApi; +import de.sovity.edc.client.oauth2.OAuth2CredentialsAuthenticator; +import de.sovity.edc.client.oauth2.OAuth2CredentialsInterceptor; +import de.sovity.edc.client.oauth2.OAuth2CredentialsStore; +import de.sovity.edc.client.oauth2.OAuth2TokenFetcher; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Builds {@link EdcClient}s. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EdcClientFactory { + + public static EdcClient newClient(EdcClientBuilder builder) { + var apiClient = new ApiClient() + .setServerIndex(null) + .setBasePath(builder.managementApiUrl()); + + if (StringUtils.isNotBlank(builder.managementApiKey())) { + apiClient.addDefaultHeader("X-Api-Key", builder.managementApiKey()); + } + + if (builder.oauth2ClientCredentials() != null) { + var tokenFetcher = new OAuth2TokenFetcher(builder.oauth2ClientCredentials()); + var handler = new OAuth2CredentialsStore(tokenFetcher); + var httpClient = apiClient.getHttpClient() + .newBuilder() + .addInterceptor(new OAuth2CredentialsInterceptor(handler)) + .authenticator(new OAuth2CredentialsAuthenticator(handler)) + .build(); + apiClient.setHttpClient(httpClient); + } + + if (builder.customConfigurer() != null) { + builder.customConfigurer().accept(apiClient); + } + + return new EdcClient( + new UiApi(apiClient), + new UseCaseApi(apiClient), + new EnterpriseEditionApi(apiClient) + ); + } +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java new file mode 100644 index 000000000..b202ed6a5 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2ClientCredentials.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class OAuth2ClientCredentials { + @NonNull + private String tokenUrl; + @NonNull + private String clientId; + @NonNull + private String clientSecret; +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java new file mode 100644 index 000000000..6a9d3486a --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsAuthenticator.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.RequiredArgsConstructor; +import okhttp3.Authenticator; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Route; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * OkHttp Authenticator: Potentially re-tries requests that failed with a 401 / 403 + * with updated access tokens. + */ +@RequiredArgsConstructor +public class OAuth2CredentialsAuthenticator implements Authenticator { + private final OAuth2CredentialsStore credentialsStore; + + @Nullable + @Override + public Request authenticate(@Nullable Route route, @NotNull Response response) { + // Skip if original request had no authentication + if (!OkHttpRequestUtils.hadBearerToken(response)) { + return null; + } + + var token = credentialsStore.getAccessToken(); + synchronized (this) { + // The synchronized Block prevents multiple parallel token refreshes + // So here the token might have changed already + var changedToken = credentialsStore.getAccessToken(); + + // If the token has changed since the request was made, use the new token. + if (!changedToken.equals(token)) { + return OkHttpRequestUtils.withBearerToken(response.request(), changedToken); + } + + // If the token hasn't changed, try to be the code path to refresh the token + var updatedToken = credentialsStore.refreshAccessToken(); + + // Retry the request with the new token. + return OkHttpRequestUtils.withBearerToken(response.request(), updatedToken); + } + } +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java new file mode 100644 index 000000000..dd52d2d8d --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.RequiredArgsConstructor; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * OkHttp Interceptor: Adds Bearer Token to requests + */ +@RequiredArgsConstructor +public class OAuth2CredentialsInterceptor implements Interceptor { + private final OAuth2CredentialsStore credentialsStore; + + @NotNull + @Override + public Response intercept(Chain chain) throws IOException { + String accessToken = credentialsStore.getAccessToken(); + Request request = OkHttpRequestUtils.withBearerToken(chain.request(), accessToken); + return chain.proceed(request); + } + +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java new file mode 100644 index 000000000..0b02da89d --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2CredentialsStore.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.SneakyThrows; + +/** + * Holds the Access Token and coordinates it between the Interceptor and the Authenticator. + */ +public class OAuth2CredentialsStore { + private final OAuth2TokenFetcher tokenFetcher; + private OAuth2TokenResponse tokenResponse = null; + + public OAuth2CredentialsStore(OAuth2TokenFetcher tokenFetcher) { + this.tokenFetcher = tokenFetcher; + this.fetchAccessTokenInternal(); + } + + public String getAccessToken() { + synchronized (this) { + if (tokenResponse == null) { + fetchAccessTokenInternal(); + } + return tokenResponse.getAccessToken(); + } + } + + public String refreshAccessToken() { + synchronized (this) { + fetchAccessTokenInternal(); + return tokenResponse.getAccessToken(); + } + } + + @SneakyThrows + private void fetchAccessTokenInternal() { + // If it crashes afterwards, the next request won't attempt to use the old token + tokenResponse = null; + tokenResponse = tokenFetcher.fetchToken(); + } + +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java new file mode 100644 index 000000000..381252d04 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenFetcher.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import de.sovity.edc.client.gen.ApiClient; +import de.sovity.edc.client.gen.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import okhttp3.Call; +import okhttp3.FormBody; +import okhttp3.Request; + +/** + * OAuth2 Token Response Fetcher for the "Client Credentials Grant" Flow + */ +@RequiredArgsConstructor +public class OAuth2TokenFetcher { + private final OAuth2ClientCredentials clientCredentials; + private final ApiClient apiClient = new ApiClient(); + + /** + * Fetch an access token for a "Client Credentials" Grant + * + * @return the token response including the access token + */ + @SneakyThrows + public OAuth2TokenResponse fetchToken() { + var formData = new FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", clientCredentials.getClientId()) + .add("client_secret", clientCredentials.getClientSecret()) + .build(); + + var request = new Request.Builder() + .url(clientCredentials.getTokenUrl()) + .post(formData) + .build(); + + // Re-use the Utils for OkHttp from the OpenAPI generator + Call call = apiClient.getHttpClient().newCall(request); + ApiResponse response = apiClient.execute(call, OAuth2TokenResponse.class); + return response.getData(); + } +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java new file mode 100644 index 000000000..5096169b9 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OAuth2TokenResponse.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import com.google.gson.annotations.SerializedName; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class OAuth2TokenResponse { + + @SerializedName("access_token") + private String accessToken; +} diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java new file mode 100644 index 000000000..10ee1ca4e --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/OkHttpRequestUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import okhttp3.Request; +import okhttp3.Response; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class OkHttpRequestUtils { + public static boolean hadBearerToken(@NonNull Response response) { + String header = response.request().header("Authorization"); + return header != null && header.startsWith("Bearer"); + } + + @NonNull + public static Request withBearerToken(@NonNull Request request, @NonNull String accessToken) { + return request.newBuilder() + .removeHeader("Authorization") + .header("Authorization", "Bearer " + accessToken) + .build(); + } +} + diff --git a/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java new file mode 100644 index 000000000..1175628e6 --- /dev/null +++ b/extensions/wrapper/clients/java-client/src/main/java/de/sovity/edc/client/oauth2/SovityKeycloakUrl.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.client.oauth2; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * Quick access to the Keycloak OAuth Token URLs for our staging and production environments. + *

      + * For ease of use of our API Wrapper Client Libraries in Use Case Applications. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SovityKeycloakUrl { + + /** + * Sovity Production Keycloak OAuth2 Token URL + */ + public static final String PRODUCTION = "https://keycloak.prod-sovity.azure.sovity.io/realms/Portal/protocol/openid-connect/token"; + + /** + * Sovity Staging Keycloak OAuth2 Token URL + */ + public static final String STAGING = "https://keycloak.stage-sovity.azure.sovity.io/realms/Portal/protocol/openid-connect/token"; +} diff --git a/extensions/wrapper/clients/typescript-client-example/.gitignore b/extensions/wrapper/clients/typescript-client-example/.gitignore new file mode 100644 index 000000000..6635cf554 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/extensions/wrapper/clients/typescript-client-example/.prettierignore b/extensions/wrapper/clients/typescript-client-example/.prettierignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/.prettierignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/extensions/wrapper/clients/typescript-client-example/.prettierrc b/extensions/wrapper/clients/typescript-client-example/.prettierrc new file mode 100644 index 000000000..da80d2bef --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/.prettierrc @@ -0,0 +1,17 @@ +{ + "useTabs": false, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/extensions/wrapper/clients/typescript-client-example/README.md b/extensions/wrapper/clients/typescript-client-example/README.md new file mode 100644 index 000000000..d8fd5aff2 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/README.md @@ -0,0 +1,27 @@ + +
      +

      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      TypeScript API Client Example

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +Example Project that consumes the TypeScript API Client Library. + +## License + +Apache License 2.0 - see [LICENSE](../../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/clients/typescript-client-example/package-lock.json b/extensions/wrapper/clients/typescript-client-example/package-lock.json new file mode 100644 index 000000000..02b8715d5 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/package-lock.json @@ -0,0 +1,3680 @@ +{ + "name": "client-ts-example", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "client-ts-example", + "version": "0.0.1", + "dependencies": { + "@sovity.de/edc-client": "file:../typescript-client" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.30.4", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.21", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.8.1", + "svelte": "^3.54.0", + "svelte-check": "^3.0.1", + "tailwindcss": "^3.3.1", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.2.3" + } + }, + "../sovity-edc-client": { + "extraneous": true + }, + "../typescript-client": { + "name": "@sovity.de/edc-client", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^18.19.22", + "prettier": "^2.8.7", + "typescript": "^4.9.5", + "vite": "^4.5.2", + "vite-plugin-dts": "^2.3.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz", + "integrity": "sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz", + "integrity": "sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.16.tgz", + "integrity": "sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz", + "integrity": "sha512-/Ofw8UXZxuzTLsNFmz1+lmarQI6ztMZ9XktvXedTbt3SNWDn0+ODTwxExLYQ/Hod91EZB4vZPQJLoqLF0jvEzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz", + "integrity": "sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz", + "integrity": "sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz", + "integrity": "sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz", + "integrity": "sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz", + "integrity": "sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz", + "integrity": "sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz", + "integrity": "sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz", + "integrity": "sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz", + "integrity": "sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz", + "integrity": "sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz", + "integrity": "sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz", + "integrity": "sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz", + "integrity": "sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz", + "integrity": "sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz", + "integrity": "sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz", + "integrity": "sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz", + "integrity": "sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz", + "integrity": "sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@sovity.de/edc-client": { + "resolved": "../typescript-client", + "link": true + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.0.0.tgz", + "integrity": "sha512-b+gkHFZgD771kgV3aO4avHFd7y1zhmMYy9i6xOK7m/rwmwaRO8gnF5zBc0Rgca80B2PMU1bKNxyBTHA14OzUAQ==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^2.2.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^1.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "1.30.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz", + "integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte": "^2.5.0", + "@types/cookie": "^0.5.1", + "cookie": "^0.5.0", + "devalue": "^4.3.1", + "esm-env": "^1.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.0", + "mrmime": "^1.0.1", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.2", + "tiny-glob": "^0.2.9", + "undici": "^5.28.3" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": "^16.14 || >=18" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0-next.0 || ^5.0.0-next.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.2.tgz", + "integrity": "sha512-Dfy0Rbl+IctOVfJvWGxrX/3m6vxPLH8o0x+8FA5QEyMUQMo4kGOVIojjryU7YomBAexOTAuYf1RT7809yDziaA==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.3", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", + "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.2.0", + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", + "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", + "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", + "dev": true + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.361.tgz", + "integrity": "sha512-VocVwjPp05HUXzf3xmL0boRn5b0iyqC7amtDww84Jb1QJNPBc7F69gJyEeXRoriLBC4a5pSyckdllrXAg4mmRA==", + "dev": true + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.16.tgz", + "integrity": "sha512-aeSuUKr9aFVY9Dc8ETVELGgkj4urg5isYx8pLf4wlGgB0vTFjxJQdHnNH6Shmx4vYYrOTLCHtRI5i1XZ9l2Zcg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.16", + "@esbuild/android-arm64": "0.17.16", + "@esbuild/android-x64": "0.17.16", + "@esbuild/darwin-arm64": "0.17.16", + "@esbuild/darwin-x64": "0.17.16", + "@esbuild/freebsd-arm64": "0.17.16", + "@esbuild/freebsd-x64": "0.17.16", + "@esbuild/linux-arm": "0.17.16", + "@esbuild/linux-arm64": "0.17.16", + "@esbuild/linux-ia32": "0.17.16", + "@esbuild/linux-loong64": "0.17.16", + "@esbuild/linux-mips64el": "0.17.16", + "@esbuild/linux-ppc64": "0.17.16", + "@esbuild/linux-riscv64": "0.17.16", + "@esbuild/linux-s390x": "0.17.16", + "@esbuild/linux-x64": "0.17.16", + "@esbuild/netbsd-x64": "0.17.16", + "@esbuild/openbsd-x64": "0.17.16", + "@esbuild/sunos-x64": "0.17.16", + "@esbuild/win32-arm64": "0.17.16", + "@esbuild/win32-ia32": "0.17.16", + "@esbuild/win32-x64": "0.17.16" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", + "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.0.tgz", + "integrity": "sha512-GXMY6t86thctyCvQq+jqElO+MKdB09BkL3hexyGP3Oi8XLKRFaJP1ud/xlWCZ9ZIa2BxHka32zhHfcuU+XsRQg==", + "dev": true, + "peerDependencies": { + "prettier": "^1.16.4 || ^2.0.0", + "svelte": "^3.2.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", + "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "node_modules/sirv": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz", + "integrity": "sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz", + "integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-check": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.2.0.tgz", + "integrity": "sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.0.3", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.3.tgz", + "integrity": "sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 14.10.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-preprocess/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", + "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.17.2", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.0.9", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1", + "sucrase": "^3.29.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz", + "integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.18.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz", + "integrity": "sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz", + "integrity": "sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.16.tgz", + "integrity": "sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz", + "integrity": "sha512-/Ofw8UXZxuzTLsNFmz1+lmarQI6ztMZ9XktvXedTbt3SNWDn0+ODTwxExLYQ/Hod91EZB4vZPQJLoqLF0jvEzA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz", + "integrity": "sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz", + "integrity": "sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz", + "integrity": "sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz", + "integrity": "sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz", + "integrity": "sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz", + "integrity": "sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz", + "integrity": "sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz", + "integrity": "sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz", + "integrity": "sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz", + "integrity": "sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz", + "integrity": "sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz", + "integrity": "sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz", + "integrity": "sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz", + "integrity": "sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz", + "integrity": "sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz", + "integrity": "sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz", + "integrity": "sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz", + "integrity": "sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==", + "dev": true, + "optional": true + }, + "@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "@sovity.de/edc-client": { + "version": "file:../typescript-client", + "requires": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^18.19.22", + "prettier": "^2.8.7", + "typescript": "^4.9.5", + "vite": "^4.5.2", + "vite-plugin-dts": "^2.3.0", + "zod": "^3.22.4" + } + }, + "@sveltejs/adapter-auto": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.0.0.tgz", + "integrity": "sha512-b+gkHFZgD771kgV3aO4avHFd7y1zhmMYy9i6xOK7m/rwmwaRO8gnF5zBc0Rgca80B2PMU1bKNxyBTHA14OzUAQ==", + "dev": true, + "requires": { + "import-meta-resolve": "^2.2.0" + } + }, + "@sveltejs/kit": { + "version": "1.30.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz", + "integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==", + "dev": true, + "requires": { + "@sveltejs/vite-plugin-svelte": "^2.5.0", + "@types/cookie": "^0.5.1", + "cookie": "^0.5.0", + "devalue": "^4.3.1", + "esm-env": "^1.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.0", + "mrmime": "^1.0.1", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.2", + "tiny-glob": "^0.2.9", + "undici": "^5.28.3" + } + }, + "@sveltejs/vite-plugin-svelte": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.2.tgz", + "integrity": "sha512-Dfy0Rbl+IctOVfJvWGxrX/3m6vxPLH8o0x+8FA5QEyMUQMo4kGOVIojjryU7YomBAexOTAuYf1RT7809yDziaA==", + "dev": true, + "requires": { + "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.3", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.4" + } + }, + "@sveltejs/vite-plugin-svelte-inspector": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", + "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "@types/cookie": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", + "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", + "dev": true + }, + "@types/pug": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", + "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", + "dev": true + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true + }, + "devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", + "dev": true + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.361.tgz", + "integrity": "sha512-VocVwjPp05HUXzf3xmL0boRn5b0iyqC7amtDww84Jb1QJNPBc7F69gJyEeXRoriLBC4a5pSyckdllrXAg4mmRA==", + "dev": true + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "esbuild": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.16.tgz", + "integrity": "sha512-aeSuUKr9aFVY9Dc8ETVELGgkj4urg5isYx8pLf4wlGgB0vTFjxJQdHnNH6Shmx4vYYrOTLCHtRI5i1XZ9l2Zcg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.16", + "@esbuild/android-arm64": "0.17.16", + "@esbuild/android-x64": "0.17.16", + "@esbuild/darwin-arm64": "0.17.16", + "@esbuild/darwin-x64": "0.17.16", + "@esbuild/freebsd-arm64": "0.17.16", + "@esbuild/freebsd-x64": "0.17.16", + "@esbuild/linux-arm": "0.17.16", + "@esbuild/linux-arm64": "0.17.16", + "@esbuild/linux-ia32": "0.17.16", + "@esbuild/linux-loong64": "0.17.16", + "@esbuild/linux-mips64el": "0.17.16", + "@esbuild/linux-ppc64": "0.17.16", + "@esbuild/linux-riscv64": "0.17.16", + "@esbuild/linux-s390x": "0.17.16", + "@esbuild/linux-x64": "0.17.16", + "@esbuild/netbsd-x64": "0.17.16", + "@esbuild/openbsd-x64": "0.17.16", + "@esbuild/sunos-x64": "0.17.16", + "@esbuild/win32-arm64": "0.17.16", + "@esbuild/win32-ia32": "0.17.16", + "@esbuild/win32-x64": "0.17.16" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-meta-resolve": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", + "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true + }, + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true + }, + "lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true + }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true + }, + "prettier-plugin-svelte": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.0.tgz", + "integrity": "sha512-GXMY6t86thctyCvQq+jqElO+MKdB09BkL3hexyGP3Oi8XLKRFaJP1ud/xlWCZ9ZIa2BxHka32zhHfcuU+XsRQg==", + "dev": true, + "requires": {} + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", + "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "requires": { + "mri": "^1.1.0" + } + }, + "sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "requires": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "sirv": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz", + "integrity": "sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + } + }, + "sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "svelte": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz", + "integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==", + "dev": true + }, + "svelte-check": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.2.0.tgz", + "integrity": "sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.0.3", + "typescript": "^5.0.3" + } + }, + "svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "requires": {} + }, + "svelte-preprocess": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.3.tgz", + "integrity": "sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==", + "dev": true, + "requires": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "dependencies": { + "magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + } + } + }, + "tailwindcss": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", + "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", + "dev": true, + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.17.2", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.0.9", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1", + "sucrase": "^3.29.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "requires": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true + }, + "ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true + }, + "undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "requires": { + "@fastify/busboy": "^2.0.0" + } + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "vite": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.3.tgz", + "integrity": "sha512-kLU+m2q0Y434Y1kCy3TchefAdtFso0ILi0dLyFV8Us3InXTU11H/B5ZTqCKIQHzSKNxVG/yEx813EA9f1imQ9A==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.18.0" + } + }, + "vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "requires": {} + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } +} diff --git a/extensions/wrapper/clients/typescript-client-example/package.json b/extensions/wrapper/clients/typescript-client-example/package.json new file mode 100644 index 000000000..b20b7ea96 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/package.json @@ -0,0 +1,33 @@ +{ + "name": "client-ts-example", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "npm run dev", + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check .", + "format": "prettier --plugin-search-dir . --write ." + }, + "dependencies": { + "@sovity.de/edc-client": "file:../typescript-client" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.30.4", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.21", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.8.1", + "svelte": "^3.54.0", + "svelte-check": "^3.0.1", + "tailwindcss": "^3.3.1", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.2.3" + }, + "type": "module" +} diff --git a/extensions/wrapper/clients/typescript-client-example/postcss.config.js b/extensions/wrapper/clients/typescript-client-example/postcss.config.js new file mode 100644 index 000000000..ba8073047 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/extensions/wrapper/clients/typescript-client-example/src/app.css b/extensions/wrapper/clients/typescript-client-example/src/app.css new file mode 100644 index 000000000..01b157e2e --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/src/app.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.btn { + @apply rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm; + @apply hover:bg-indigo-500; + @apply focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600; +} diff --git a/extensions/wrapper/clients/typescript-client-example/src/app.d.ts b/extensions/wrapper/clients/typescript-client-example/src/app.d.ts new file mode 100644 index 000000000..899c7e8fc --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/extensions/wrapper/clients/typescript-client-example/src/app.html b/extensions/wrapper/clients/typescript-client-example/src/app.html new file mode 100644 index 000000000..117bd0261 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
      %sveltekit.body%
      + + diff --git a/extensions/wrapper/clients/typescript-client-example/src/routes/+layout.svelte b/extensions/wrapper/clients/typescript-client-example/src/routes/+layout.svelte new file mode 100644 index 000000000..4039a26b2 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/extensions/wrapper/clients/typescript-client-example/src/routes/+page.svelte b/extensions/wrapper/clients/typescript-client-example/src/routes/+page.svelte new file mode 100644 index 000000000..c7740d42b --- /dev/null +++ b/extensions/wrapper/clients/typescript-client-example/src/routes/+page.svelte @@ -0,0 +1,75 @@ + + +
      +
      +

      + Example TypeScript Client Usage +

      +

      + {#await kpiData} + Loading... + {:then data} + Successfully fetched KPI Endpoint from our EDC API Wrapper Use Case API. + + + + + + + + + {#each Object.entries(data) as [key, value]} + + + + + {/each} + +
      FieldValue
      {key}{JSON.stringify(value)}
      + {:catch err} + Error fetching KPI Endpoint, see console. + {/await} +

      +
      + +
      +
      +
      + + diff --git a/extensions/wrapper/clients/typescript-client-example/static/favicon.png b/extensions/wrapper/clients/typescript-client-example/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH +
      +
      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      TypeScript API Client

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +TypeScript Client Library to be imported and used in arbitrary applications like +frontends or NodeJS projects. + +You can find our API Wrapper Project +[here](https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper). + +## How to install + +Requires global fetch API (provided by Node.js 18+ or browser). + +```shell script +npm i --save @sovity.de/edc-client +``` + +## How to use + +Configure your EDC Client and use endpoints of our API Wrapper Extension: + +### Example Using API Key Auth + +```typescript +const edcClient: EdcClient = buildEdcClient({ + managementApiUrl: 'http://localhost:11002/api/management/v2', + managementApiKey: 'ApiKeyDefaultValue', +}); + +let kpiData: KpiResult = await edcClient.useCaseApi.getKpis(); +``` + +A minimal example project using the typescript API client can be found +[here](https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper/clients/typescript-client-example). + +### Example Using OAuth2 Client Credentials + +```typescript +const edcClient: EdcClient = buildEdcClient({ + managementApiUrl: 'http://localhost:11002/api/management/v2', + clientCredentials: { + tokenUrl: 'http://localhost:11002/token', + clientId: '{{your-connector}}-app', + clientSecret: '...', + }, +}); + +let kpiData: KpiResult = await edcClient.useCaseApi.getKpis(); +``` + +## License + +Apache License 2.0 - see +[LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/clients/typescript-client/index.html b/extensions/wrapper/clients/typescript-client/index.html new file mode 100644 index 000000000..f49a7cf04 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/index.html @@ -0,0 +1,12 @@ + + + + + + Client example + + +
      + + + diff --git a/extensions/wrapper/clients/typescript-client/package-lock.json b/extensions/wrapper/clients/typescript-client/package-lock.json new file mode 100644 index 000000000..28e667c34 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/package-lock.json @@ -0,0 +1,3222 @@ +{ + "name": "@sovity.de/edc-client", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@sovity.de/edc-client", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^18.19.22", + "prettier": "^2.8.7", + "typescript": "^4.9.5", + "vite": "^4.5.2", + "vite-plugin-dts": "^2.3.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.34.4.tgz", + "integrity": "sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz", + "integrity": "sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "3.55.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", + "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "dev": true, + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", + "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "dev": true, + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", + "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "dev": true, + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.23.2", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.22.tgz", + "integrity": "sha512-p3pDIfuMg/aXBmhkyanPshdfJuX5c5+bQjYLIikPLXAUycEogij/c50n/C+8XOA5L93cU4ZRXtn+dNQGi0IZqQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kolorist": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.7.0.tgz", + "integrity": "sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validator": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.3.0.tgz", + "integrity": "sha512-WbJgGtsStgQhdm3EosYmIdTGbag5YQpZ3HXWUAPCDyoXI5qN6EY0V7NXq0lAmnv9hVQsvh0htbYcg0Or5Db9JQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.21.4", + "@microsoft/api-extractor": "^7.34.4", + "@rollup/pluginutils": "^5.0.2", + "@rushstack/node-core-library": "^3.55.2", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "kolorist": "^1.7.0", + "magic-string": "^0.29.0", + "ts-morph": "18.0.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": ">=2.9.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + } + }, + "@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "dependencies": { + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "requires": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@microsoft/api-extractor": { + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.34.4.tgz", + "integrity": "sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "dependencies": { + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz", + "integrity": "sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@rushstack/node-core-library": { + "version": "3.55.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", + "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "dev": true, + "requires": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", + "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "dev": true, + "requires": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "@rushstack/ts-command-line": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", + "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@trivago/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", + "dev": true, + "requires": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.23.2", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + } + }, + "@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "dev": true, + "requires": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "@types/node": { + "version": "18.19.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.22.tgz", + "integrity": "sha512-p3pDIfuMg/aXBmhkyanPshdfJuX5c5+bQjYLIikPLXAUycEogij/c50n/C+8XOA5L93cU4ZRXtn+dNQGi0IZqQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "kolorist": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.7.0.tgz", + "integrity": "sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "dev": true, + "requires": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "validator": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "dev": true + }, + "vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, + "vite-plugin-dts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.3.0.tgz", + "integrity": "sha512-WbJgGtsStgQhdm3EosYmIdTGbag5YQpZ3HXWUAPCDyoXI5qN6EY0V7NXq0lAmnv9hVQsvh0htbYcg0Or5Db9JQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.21.4", + "@microsoft/api-extractor": "^7.34.4", + "@rollup/pluginutils": "^5.0.2", + "@rushstack/node-core-library": "^3.55.2", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "kolorist": "^1.7.0", + "magic-string": "^0.29.0", + "ts-morph": "18.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "requires": { + "commander": "^9.4.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } + }, + "zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==" + } + } +} diff --git a/extensions/wrapper/clients/typescript-client/package.json b/extensions/wrapper/clients/typescript-client/package.json new file mode 100644 index 000000000..905f3ea54 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/package.json @@ -0,0 +1,56 @@ +{ + "name": "@sovity.de/edc-client", + "version": "0.0.0", + "description": "TypeScript API Client for our EDC API Wrapper.", + "author": "sovity GmbH", + "license": "Apache-2.0", + "homepage": "https://sovity.de", + "repository": { + "type": "git", + "url": "https://github.com/sovity/edc-extensions/" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "bugs": { + "url": "https://github.com/sovity/edc-extensions/issues/new/choose" + }, + "keywords": [ + "sovity", + "api client", + "edc", + "eclipse dataspace components", + "mobility data space", + "Catena-X" + ], + "type": "module", + "main": "./dist/sovity-edc-client.umd.cjs", + "module": "./dist/sovity-edc-client.js", + "types": "./dist/sovity-edc-client.d.ts", + "exports": { + ".": { + "import": "./dist/sovity-edc-client.js", + "require": "./dist/sovity-edc-client.umd.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "npm run format-all && tsc && vite build", + "preview": "vite preview", + "format-all": "prettier --write ." + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^18.19.22", + "prettier": "^2.8.7", + "typescript": "^4.9.5", + "vite": "^4.5.2", + "vite-plugin-dts": "^2.3.0" + }, + "dependencies": { + "zod": "^3.22.4" + } +} diff --git a/extensions/wrapper/clients/typescript-client/prettier.config.cjs b/extensions/wrapper/clients/typescript-client/prettier.config.cjs new file mode 100644 index 000000000..fbcf1dc96 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/prettier.config.cjs @@ -0,0 +1,26 @@ +module.exports = { + tabWidth: 4, + useTabs: false, + singleQuote: true, + semi: true, + arrowParens: 'always', + trailingComma: 'all', + bracketSameLine: true, + printWidth: 80, + bracketSpacing: false, + proseWrap: 'always', + + // @trivago/prettier-plugin-sort-imports + importOrder: [ + '', + // rest after + '^[./]', + ], + importOrderParserPlugins: [ + 'typescript', + 'classProperties', + 'decorators-legacy', + ], + importOrderSeparation: false, + importOrderSortSpecifiers: true, +}; diff --git a/extensions/wrapper/clients/typescript-client/src/EdcClient.ts b/extensions/wrapper/clients/typescript-client/src/EdcClient.ts new file mode 100644 index 000000000..f31a5dfeb --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/EdcClient.ts @@ -0,0 +1,65 @@ +import { + Configuration, + ConfigurationParameters, + EnterpriseEditionApi, + Middleware, + UIApi, + UseCaseApi, +} from './generated'; +import {buildOAuthMiddleware} from './oauth2/Middleware'; +import {ClientCredentials} from './oauth2/model/ClientCredentials'; + +/** + * API Client for our sovity EDC + */ +export interface EdcClient { + uiApi: UIApi; + useCaseApi: UseCaseApi; + enterpriseEditionApi: EnterpriseEditionApi; +} + +/** + * Configure & Build new EDC Client + * @param opts opts + */ +export function buildEdcClient(opts: EdcClientOptions): EdcClient { + let middleware: Middleware[] = []; + let headers: Record = {}; + + if (opts.clientCredentials) { + middleware.push(buildOAuthMiddleware(opts.clientCredentials)); + } + if (opts.managementApiKey) { + headers = buildApiKeyHeader(opts.managementApiKey); + } + + const config = new Configuration({ + basePath: opts.managementApiUrl, + headers, + credentials: 'same-origin', + middleware, + ...opts.configOverrides, + }); + + return { + uiApi: new UIApi(config), + useCaseApi: new UseCaseApi(config), + enterpriseEditionApi: new EnterpriseEditionApi(config), + }; +} + +function buildApiKeyHeader(key: string) { + return { + 'X-Api-Key': key, + }; +} + +/** + * Options for instantiating an EDC API Client + */ +export interface EdcClientOptions { + managementApiUrl: string; + managementApiKey?: string; + clientCredentials?: ClientCredentials; + configOverrides?: Partial; +} diff --git a/extensions/wrapper/clients/typescript-client/src/generated/.gitignore b/extensions/wrapper/clients/typescript-client/src/generated/.gitignore new file mode 100644 index 000000000..654617bb0 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/generated/.gitignore @@ -0,0 +1,2 @@ +**/* +!.gitignore diff --git a/extensions/wrapper/clients/typescript-client/src/index.ts b/extensions/wrapper/clients/typescript-client/src/index.ts new file mode 100644 index 000000000..3edca50b2 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/index.ts @@ -0,0 +1,2 @@ +export * from './EdcClient'; +export * from './generated'; diff --git a/extensions/wrapper/clients/typescript-client/src/oauth2/AccessTokenService.ts b/extensions/wrapper/clients/typescript-client/src/oauth2/AccessTokenService.ts new file mode 100644 index 000000000..535d622ec --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/oauth2/AccessTokenService.ts @@ -0,0 +1,47 @@ +import {ClientCredentials} from './model/ClientCredentials'; +import { + ClientCredentialsResponse, + fetchClientCredentials, +} from './utils/FetchUtils'; + +export class AccessTokenService { + private activeRequest: Promise | null = null; + private refreshTimeout?: NodeJS.Timeout; + private accessToken: string | null = null; + + constructor(private clientCredentials: ClientCredentials) {} + + async getAccessToken(): Promise { + if (!this.accessToken) { + return await this.refreshAccessToken(); + } + return this.accessToken; + } + + /** + * Synchronized refreshing of the access token + */ + async refreshAccessToken(): Promise { + if (this.activeRequest) { + await this.activeRequest; + return this.accessToken!; + } + + this.accessToken = null; + this.activeRequest = fetchClientCredentials(this.clientCredentials); + const response = await this.activeRequest; + this.scheduleNextRefresh(response); + this.accessToken = response.access_token; + this.activeRequest = null; + + return this.accessToken; + } + + private scheduleNextRefresh(response: ClientCredentialsResponse) { + clearTimeout(this.refreshTimeout); + const ms = (response.expires_in - 2) * 1000; + this.refreshTimeout = setTimeout(() => { + this.accessToken = null; + }, ms); + } +} diff --git a/extensions/wrapper/clients/typescript-client/src/oauth2/Middleware.ts b/extensions/wrapper/clients/typescript-client/src/oauth2/Middleware.ts new file mode 100644 index 000000000..1ee5e2062 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/oauth2/Middleware.ts @@ -0,0 +1,34 @@ +import {Middleware, RequestContext, ResponseContext} from '../generated'; +import {AccessTokenService} from './AccessTokenService'; +import {ClientCredentials} from './model/ClientCredentials'; +import {needsAuthentication} from './utils/HttpUtils'; +import {injectAccessTokenHeader} from './utils/RequestUtils'; + +export function buildOAuthMiddleware( + clientCredentials: ClientCredentials, +): Middleware { + const accessTokenService = new AccessTokenService(clientCredentials); + + return { + pre: async (ctx: RequestContext) => { + const accessToken = await accessTokenService.getAccessToken(); + injectAccessTokenHeader(ctx.init, accessToken); + + return Promise.resolve({ + url: ctx.url, + init: ctx.init, + }); + }, + post: async (ctx: ResponseContext): Promise => { + if (needsAuthentication(ctx.response.status)) { + const accessToken = + await accessTokenService.refreshAccessToken(); + injectAccessTokenHeader(ctx.init, accessToken); + // Use normal fetch to not trigger middleware on retry + return await fetch(ctx.url, ctx.init); + } + + return Promise.resolve(ctx.response); + }, + }; +} diff --git a/extensions/wrapper/clients/typescript-client/src/oauth2/model/ClientCredentials.ts b/extensions/wrapper/clients/typescript-client/src/oauth2/model/ClientCredentials.ts new file mode 100644 index 000000000..1b2d8252e --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/oauth2/model/ClientCredentials.ts @@ -0,0 +1,8 @@ +/** + * Credentials for connecting to the EDC via the OAuth2 "Client Credentials" flow. + */ +export interface ClientCredentials { + tokenUrl: string; + clientId: string; + clientSecret: string; +} diff --git a/extensions/wrapper/clients/typescript-client/src/oauth2/utils/FetchUtils.ts b/extensions/wrapper/clients/typescript-client/src/oauth2/utils/FetchUtils.ts new file mode 100644 index 000000000..21366ecd9 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/oauth2/utils/FetchUtils.ts @@ -0,0 +1,39 @@ +import {z} from 'zod'; +import {ClientCredentials} from '../model/ClientCredentials'; +import {createUrlEncodedParamsString} from './HttpUtils'; + +const ClientCredentialsResponseSchema = z.object({ + access_token: z.string().min(1), + token_type: z.string(), + expires_in: z.number(), + scope: z.string(), +}); +export type ClientCredentialsResponse = z.infer< + typeof ClientCredentialsResponseSchema +>; + +export async function fetchClientCredentials( + clientCredentials: ClientCredentials, +): Promise { + let response = await fetch(clientCredentials.tokenUrl, { + method: 'POST', + body: createUrlEncodedParamsString({ + grant_type: 'client_credentials', + client_id: clientCredentials.clientId, + client_secret: clientCredentials.clientSecret, + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + let json: unknown = await response.json(); + return parseTokenResponse(json); +} + +function parseTokenResponse(json: unknown): ClientCredentialsResponse { + const tokenResponseParsed = ClientCredentialsResponseSchema.safeParse(json); + if (!tokenResponseParsed.success) { + throw new Error('Bad access token response'); + } + return tokenResponseParsed.data; +} diff --git a/extensions/wrapper/clients/typescript-client/src/oauth2/utils/HttpUtils.ts b/extensions/wrapper/clients/typescript-client/src/oauth2/utils/HttpUtils.ts new file mode 100644 index 000000000..a4e836b74 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/oauth2/utils/HttpUtils.ts @@ -0,0 +1,13 @@ +/** + * Checks if the given HTTP status code is either 401 (Unauthorized) or 403 (Forbidden). + * @param httpStatus + */ +export function needsAuthentication(httpStatus: number) { + return httpStatus === 401 || httpStatus === 403; +} + +export function createUrlEncodedParamsString(obj: Record) { + return Object.entries(obj) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); +} diff --git a/extensions/wrapper/clients/typescript-client/src/oauth2/utils/RequestUtils.ts b/extensions/wrapper/clients/typescript-client/src/oauth2/utils/RequestUtils.ts new file mode 100644 index 000000000..0b8a8fe89 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/oauth2/utils/RequestUtils.ts @@ -0,0 +1,44 @@ +export function injectAccessTokenHeader(init: RequestInit, token: string) { + init.headers = withHeader('Authorization', `Bearer ${token}`, init.headers); +} + +function withHeader( + headerName: string, + headerValue: string, + headers?: HeadersInit, +): HeadersInit { + if (!headers) { + headers = {}; + headers[headerName] = headerValue; + return headers; + } + + if (Array.isArray(headers)) { + return headers.map(([a, b]) => + a !== headerName ? [a, b] : [headerName, headerValue], + ); + } + + if (isHeaders(headers)) { + if (headers.has(headerName)) { + headers.set(headerName, headerValue); + } else { + headers.append(headerName, headerValue); + } + return headers; + } + + headers[headerName] = headerValue; + return headers; +} + +function isHeaders(object: any): object is Headers { + return ( + 'append' in object && + 'delete' in object && + 'get' in object && + 'has' in object && + 'set' in object && + 'forEach' in object + ); +} diff --git a/extensions/wrapper/clients/typescript-client/src/vite-env.d.ts b/extensions/wrapper/clients/typescript-client/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/extensions/wrapper/clients/typescript-client/tsconfig.json b/extensions/wrapper/clients/typescript-client/tsconfig.json new file mode 100644 index 000000000..138755d01 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/extensions/wrapper/clients/typescript-client/vite.config.ts b/extensions/wrapper/clients/typescript-client/vite.config.ts new file mode 100644 index 000000000..96c00ae85 --- /dev/null +++ b/extensions/wrapper/clients/typescript-client/vite.config.ts @@ -0,0 +1,17 @@ +// noinspection JSUnusedGlobalSymbols +import {resolve} from 'path'; +import {defineConfig} from 'vite'; +import dts from 'vite-plugin-dts'; + +// https://vitejs.dev/guide/build.html#library-mode +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'sovity-edc-client', + fileName: 'sovity-edc-client', + }, + sourcemap: true, + }, + plugins: [dts({rollupTypes: true})], +}); diff --git a/extensions/wrapper/wrapper-api/README.md b/extensions/wrapper/wrapper-api/README.md new file mode 100644 index 000000000..f1dde77d4 --- /dev/null +++ b/extensions/wrapper/wrapper-api/README.md @@ -0,0 +1,27 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Wrapper API Specification

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +All APIs to be included in our generated client libraries. + +## License + +Apache License 2.0 - see [LICENSE](../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/wrapper-api/build.gradle.kts b/extensions/wrapper/wrapper-api/build.gradle.kts new file mode 100644 index 000000000..2c9b125e6 --- /dev/null +++ b/extensions/wrapper/wrapper-api/build.gradle.kts @@ -0,0 +1,98 @@ +val edcVersion: String by project +val edcGroup: String by project +val restAssured: String by project +val assertj: String by project +val mockitoVersion: String by project +val lombokVersion: String by project +val jettyVersion: String by project +val jettyGroup: String by project + +plugins { + `java-library` + `maven-publish` + id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.14" //./gradlew clean resolve + id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI + id("org.openapi.generator") version "6.6.0" //./gradlew openApiValidate && ./gradlew openApiGenerate +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api(project(":extensions:wrapper:wrapper-common-api")) + api(project(":extensions:wrapper:wrapper-common-mappers")) + api(project(":extensions:wrapper:wrapper-ee-api")) + + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") + implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") + implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation("org.apache.commons:commons-lang3:3.13.0") +} + +val openapiFileDir = "${project.buildDir}/swagger" +val openapiFileFilename = "sovity-edc-api-wrapper.yaml" +val openapiFile = "$openapiFileDir/$openapiFileFilename" + +val openapiDocsDir = project.rootProject.rootDir.resolve("docs") + +tasks.withType { + outputDir = file(openapiFileDir) + outputFileName = openapiFileFilename.removeSuffix(".yaml") + prettyPrint = true + outputFormat = io.swagger.v3.plugins.gradle.tasks.ResolveTask.Format.YAML + classpath = java.sourceSets["main"].runtimeClasspath + buildClasspath = classpath + resourcePackages = setOf("de.sovity.edc.ext.wrapper.api") +} + +tasks.register("copyOpenapiYamlToDocs") { + dependsOn("resolve") + from(openapiFile) + into(openapiDocsDir) +} + +task("openApiGenerateTypeScriptClient") { + dependsOn("resolve") + dependsOn("copyOpenapiYamlToDocs") + generatorName.set("typescript-fetch") + configOptions.set(mutableMapOf( + "supportsES6" to "true", + "npmVersion" to "8.15.0", + "typescriptThreePlus" to "true", + )) + + inputSpec.set(openapiFile) + val outputDirectory = buildFile.parentFile.resolve("../clients/typescript-client/src/generated").normalize() + outputDir.set(outputDirectory.toString()) + + doFirst { + project.delete(fileTree(outputDirectory).exclude("**/.gitignore")) + } + + doLast { + outputDirectory.resolve("src/generated").renameTo(outputDirectory) + } +} + +tasks.withType { + dependsOn("resolve") + dependsOn("openApiGenerateTypeScriptClient") + from(openapiFileDir) { + include(openapiFileFilename) + } +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java new file mode 100644 index 000000000..f04c04721 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.servers.Server; + +@OpenAPIDefinition( + info = @Info( + title = "sovity EDC API Wrapper", + version = "0.0.0", + description = "sovity's EDC API Wrapper contains a selection of APIs for multiple consumers, " + + "e.g. our EDC UI API, our generic Use Case API, our Commercial APIs, etc. " + + "We bundled these APIs, so we can have an easier time generating our API Client Libraries.", + contact = @Contact( + name = "Sovity GmbH", + email = "contact@sovity.de", + url = "https://github.com/sovity/edc-extensions/issues/new/choose" + ), + license = @License( + name = "Apache 2.0", + url = "https://github.com/sovity/edc-extensions/blob/main/LICENSE" + ) + ), + servers = { + @Server(url = "https://my-connector/api/management") + }, + externalDocs = @ExternalDocumentation( + description = "EDC API Wrapper Project in sovity/edc-extensions", + url = "https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper" + ) +) +public interface ApiInformation { +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java new file mode 100644 index 000000000..253c7dbbc --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui; + +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionPage; +import de.sovity.edc.ext.wrapper.api.ui.model.TransferHistoryPage; +import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + +@Path("wrapper/ui") +@Tag(name = "UI", description = "EDC UI API Endpoints") +interface UiResource { + + @GET + @Path("pages/dashboard-page") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Collect all data for the Dashboard Page") + DashboardPage getDashboardPage(); + + @GET + @Path("pages/asset-page") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Collect all data for Asset Page") + AssetPage getAssetPage(); + + @POST + @Path("pages/asset-page/assets") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new Asset") + IdResponseDto createAsset(UiAssetCreateRequest uiAssetCreateRequest); + + @PUT + @Path("pages/asset-page/assets/{assetId}/metadata") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Updates an Asset's metadata") + IdResponseDto editAssetMetadata(@PathParam("assetId") String assetId, UiAssetEditMetadataRequest uiAssetEditMetadataRequest); + + @DELETE + @Path("pages/asset-page/assets/{assetId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Delete an Asset") + IdResponseDto deleteAsset(@PathParam("assetId") String assetId); + + @GET + @Path("pages/policy-page") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Collect all data for Policy Definition Page") + PolicyDefinitionPage getPolicyDefinitionPage(); + + @POST + @Path("pages/policy-page/policy-definitions") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new Policy Definition") + IdResponseDto createPolicyDefinition(PolicyDefinitionCreateRequest policyDefinitionDtoDto); + + @DELETE + @Path("pages/policy-page/policy-definitions/{policyId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Delete a Policy Definition") + IdResponseDto deletePolicyDefinition(@PathParam("policyId") String policyId); + + @GET + @Path("pages/contract-definition-page") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Collect all data for Contract Definition Page") + ContractDefinitionPage getContractDefinitionPage(); + + @POST + @Path("pages/contract-definition-page/contract-definitions") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new Contract Definition") + IdResponseDto createContractDefinition(ContractDefinitionRequest contractDefinitionRequest); + + @DELETE + @Path("pages/contract-definition-page/contract-definitions/{contractDefinitionId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Delete a Contract Definition") + IdResponseDto deleteContractDefinition(@PathParam("contractDefinitionId") String contractDefinitionId); + + @GET + @Path("pages/catalog-page/data-offers") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Fetch a connector's data offers") + List getCatalogPageDataOffers(@QueryParam("connectorEndpoint") String connectorEndpoint); + + @POST + @Path("pages/catalog-page/contract-negotiations") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Initiate a new Contract Negotiation") + UiContractNegotiation initiateContractNegotiation(ContractNegotiationRequest contractNegotiationRequest); + + @GET + @Path("pages/catalog-page/contract-negotiations/{contractNegotiationId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Get Contract Negotiation Information") + UiContractNegotiation getContractNegotiation(@PathParam("contractNegotiationId") String contractNegotiationId); + + @GET + @Path("pages/contract-agreement-page") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Collect all data for the Contract Agreement Page") + ContractAgreementPage getContractAgreementPage(); + + @POST + @Path("pages/contract-agreement-page/transfers") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Initiate a Transfer Process") + IdResponseDto initiateTransfer(InitiateTransferRequest initiateTransferRequest); + + @POST + @Path("pages/contract-agreement-page/transfers/custom") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Initiate a Transfer Process via a custom Transfer Process JSON-LD. Fields such as connectorId, assetId, providerConnectorId, providerConnectorAddress will be set automatically.") + IdResponseDto initiateCustomTransfer(InitiateCustomTransferRequest initiateCustomTransferRequest); + + @GET + @Path("pages/transfer-history-page") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Collect all data for the Transfer History Page") + TransferHistoryPage getTransferHistoryPage(); + + @GET + @Path("pages/transfer-history-page/transfer-processes/{transferProcessId}/asset") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Queries a transfer process' asset") + UiAsset getTransferProcessAsset(@PathParam("transferProcessId") String transferProcessId); +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java new file mode 100644 index 000000000..bcd792e22 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@Schema(description = "All data for the Asset Page") +public class AssetPage { + @Schema(description = "Visible Assets", requiredMode = Schema.RequiredMode.REQUIRED) + private List assets; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java new file mode 100644 index 000000000..d4d543299 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.List; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Contract Agreement for Contract Agreement Page") +public class ContractAgreementCard { + @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractAgreementId; + + @Schema(description = "Contract Negotiation ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractNegotiationId; + + @Schema(description = "Incoming vs Outgoing", requiredMode = Schema.RequiredMode.REQUIRED) + private ContractAgreementDirection direction; + + @Schema(description = "Other Connector's Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String counterPartyAddress; + + @Schema(description = "Other Connector's ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String counterPartyId; + + @Schema(description = "Contract Agreements Signing Date", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime contractSigningDate; + + @Schema(description = "Asset details", requiredMode = Schema.RequiredMode.REQUIRED) + private UiAsset asset; + + @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicy contractPolicy; + + @Schema(description = "Contract Agreement's Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) + private List transferProcesses; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementDirection.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementDirection.java new file mode 100644 index 000000000..ef40b8edd --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementDirection.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; + +import java.util.Arrays; + +@Getter +@RequiredArgsConstructor +@Schema(description = "Whether the contract agreement is incoming or outgoing", enumAsRef = true) +public enum ContractAgreementDirection { + CONSUMING(ContractNegotiation.Type.CONSUMER), + PROVIDING(ContractNegotiation.Type.PROVIDER); + + private final ContractNegotiation.Type type; + + public static ContractAgreementDirection fromType(ContractNegotiation.Type type) { + return Arrays.stream(values()).filter(it -> it.type == type) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "No ContractAgreementType for type %s.".formatted(type) + )); + } +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java new file mode 100644 index 000000000..915ae73fc --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class ContractAgreementPage { + @Schema(description = "Contract Agreement Cards", requiredMode = Schema.RequiredMode.REQUIRED) + private List contractAgreements; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java new file mode 100644 index 000000000..6c6dbcf6a --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "A Contract Agreement's Transfer Process") +public class ContractAgreementTransferProcess { + @Schema(description = "Transfer Process ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String transferProcessId; + @Schema(description = "Last Change Date", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime lastUpdatedDate; + @Schema(description = "Current State", requiredMode = Schema.RequiredMode.REQUIRED) + private TransferProcessState state; + @Schema(description = "Error Message") + private String errorMessage; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java new file mode 100644 index 000000000..a19650dcf --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "Contract Definition List Entry for Contract Definition Page") +public class ContractDefinitionEntry { + + @Schema(description = "Contract Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractDefinitionId; + + @Schema(description = "Access Policy ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String accessPolicyId; + + @Schema(description = "Contract Policy ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractPolicyId; + + @Schema(description = "Criteria for the contract", requiredMode = Schema.RequiredMode.REQUIRED) + private List assetSelector; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java new file mode 100644 index 000000000..737fb2b36 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@Schema(description = "All data for the Contract Definition Page") +public class ContractDefinitionPage { + + @Schema(description = "Contract Definition Entries", requiredMode = Schema.RequiredMode.REQUIRED) + private List contractDefinitions; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java new file mode 100644 index 000000000..a4b603970 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java @@ -0,0 +1,46 @@ + +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Data for creating a Contract Definition") +public class ContractDefinitionRequest { + + @Schema(description = "Contract Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractDefinitionId; + + @Schema(description = "Contract Policy ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractPolicyId; + + @Schema(description = "Access Policy ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String accessPolicyId; + + @Schema(description = "List of Criteria for the contract", requiredMode = Schema.RequiredMode.REQUIRED) + private List assetSelector; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java new file mode 100644 index 000000000..1ab5135be --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java @@ -0,0 +1,47 @@ + +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Data for initiating a Contract Negotiation") +public class ContractNegotiationRequest { + + @Schema(description = "Counter Party Address", requiredMode = Schema.RequiredMode.REQUIRED) + private String counterPartyAddress; + + @Schema(description = "Counter Party Participant ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String counterPartyParticipantId; + + @Schema(description = "Contract Offer Dto ", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractOfferId; + + @Schema(description = "Policy JsonLd", requiredMode = Schema.RequiredMode.REQUIRED) + private String policyJsonLd; + + @Schema(description = "Asset ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetId; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationSimplifiedState.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationSimplifiedState.java new file mode 100644 index 000000000..2e5e9e28c --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationSimplifiedState.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * This class exists because we want to highlight either running or failed contract negotiations in our UI. + *

      + * That distinction has to be made somewhere. Let's rather do that distinction in the backend. + */ +@Schema(description = "Simplified Contract Negotiation State to be used in UI", enumAsRef = true) +public enum ContractNegotiationSimplifiedState { + IN_PROGRESS, + AGREED, + TERMINATED +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java new file mode 100644 index 000000000..b4aab9343 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Contract Negotiation State interpreted") +public class ContractNegotiationState { + @Schema(description = "State name or 'CUSTOM'. State names only exist for original EDC Contract Negotiation States.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + @Schema(description = "State code", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer code; + @Schema(description = "Whether we are running, in an error state or done.", requiredMode = Schema.RequiredMode.REQUIRED) + private ContractNegotiationSimplifiedState simplifiedState; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java new file mode 100644 index 000000000..c6feb776f --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java @@ -0,0 +1,17 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@Schema(description = "DAPS Config") +public class DashboardDapsConfig { + @Schema(description = "Your Connector's DAPS Token URL", requiredMode = Schema.RequiredMode.REQUIRED) + private String tokenUrl; + + @Schema(description = "Your Connector's DAPS JWKS URL", requiredMode = Schema.RequiredMode.REQUIRED) + private String jwksUrl; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardMiwConfig.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardMiwConfig.java new file mode 100644 index 000000000..05b24fa1d --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardMiwConfig.java @@ -0,0 +1,20 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@Schema(description = "Managed Identity Wallet (MIW) Config") +public class DashboardMiwConfig { + @Schema(description = "Your Connector's MIW's URL", requiredMode = Schema.RequiredMode.REQUIRED) + private String url; + + @Schema(description = "Your Connector's MIW's Token URL", requiredMode = Schema.RequiredMode.REQUIRED) + private String tokenUrl; + + @Schema(description = "Your Connector's MIW's Authority ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String authorityId; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java new file mode 100644 index 000000000..16b69fe61 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java @@ -0,0 +1,68 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +public class DashboardPage { + + @Schema(description = "Number of Assets", requiredMode = Schema.RequiredMode.REQUIRED) + private int numAssets; + + @Schema(description = "Number of Policies", requiredMode = Schema.RequiredMode.REQUIRED) + private int numPolicies; + + @Schema(description = "Number of Contract Definitions", requiredMode = Schema.RequiredMode.REQUIRED) + private int numContractDefinitions; + + @Schema(description = "Number of consuming Contract Agreements", requiredMode = Schema.RequiredMode.REQUIRED) + private long numContractAgreementsConsuming; + + @Schema(description = "Number of providing Contract Agreements", requiredMode = Schema.RequiredMode.REQUIRED) + private long numContractAgreementsProviding; + + @Schema(description = "Consuming Transfer Process Amounts", requiredMode = Schema.RequiredMode.REQUIRED) + private DashboardTransferAmounts transferProcessesConsuming; + + @Schema(description = "Providing Transfer Process Amounts", requiredMode = Schema.RequiredMode.REQUIRED) + private DashboardTransferAmounts transferProcessesProviding; + + @Schema(description = "Your Connector's Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + + @Schema(description = "Your Connector's Participant ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorParticipantId; + + @Schema(description = "Your Connector's Title", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorTitle; + + @Schema(description = "Your Connector's Description", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorDescription; + + @Schema(description = "Your Organization Homepage", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorCuratorUrl; + + @Schema(description = "Your Organization Name", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorCuratorName; + + @Schema(description = "Your Connector's Maintainer's Organization Homepage", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorMaintainerUrl; + + @Schema(description = "Your Connector's Maintainer's Organization Name", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorMaintainerName; + + @Schema(description = "Your Connector's DAPS Configuration (if present)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private DashboardDapsConfig connectorDapsConfig; + + @Schema(description = "Your Connector's MIW Configuration (if present)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private DashboardMiwConfig connectorMiwConfig; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java new file mode 100644 index 000000000..476491e78 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java @@ -0,0 +1,28 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +public class DashboardTransferAmounts { + @Schema(description = "Number of Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) + private long numTotal; + + @Schema(description = "Number of running Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) + private long numRunning; + + @Schema(description = "Number of successful Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) + private long numOk; + + @Schema(description = "Number of failed Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) + private long numError; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java new file mode 100644 index 000000000..78b1f17a0 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Marks the operation as successful") +public class IdResponseDto { + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) + private final String id; + @Schema(description = "Change Date", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime lastUpdatedDate = OffsetDateTime.now(); +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java new file mode 100644 index 000000000..5a6b87d81 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Required data for starting a Contract Agreement's Transfer Process") +public class InitiateCustomTransferRequest { + @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractAgreementId; + + @Schema(description = "Partial TransferProcessRequestJsonLd JSON-LD. Fields participantId, connectorEndpoint, " + + "assetId and contractId can be omitted, they will be overridden with information from the contract.", + requiredMode = Schema.RequiredMode.REQUIRED) + private String transferProcessRequestJsonLd; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java new file mode 100644 index 000000000..634ba0ddc --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.Map; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "For type PARAMS_ONLY: Required data for starting a Transfer Process") +public class InitiateTransferRequest { + @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractAgreementId; + + @Schema(description = "Data Sink / Data Address", requiredMode = Schema.RequiredMode.REQUIRED) + private Map dataSinkProperties; + + @Schema(description = "Additional transfer process properties. These are not passed to the consumer EDC", requiredMode = Schema.RequiredMode.REQUIRED) + private Map transferProcessProperties; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java new file mode 100644 index 000000000..3791f2df5 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@Schema(description = "All data for the policy definition page as required by the UI", requiredMode = Schema.RequiredMode.REQUIRED) +public class PolicyDefinitionPage { + @Schema(description = "Policy Definition Entries", requiredMode = Schema.RequiredMode.REQUIRED) + private List policies; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java new file mode 100644 index 000000000..8de3d4160 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.OffsetDateTime; + +@Data +@Schema(description = "Transfer History Entry for Transfer History Page") +public class TransferHistoryEntry { + @Schema(description = "Transfer Process ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String transferProcessId; + + @Schema(description = "Created Date", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdDate; + + @Schema(description = "Last Change Date", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime lastUpdatedDate; + + @Schema(description = "Transfer History State", requiredMode = Schema.RequiredMode.REQUIRED) + private TransferProcessState state; + + @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractAgreementId; + + @Schema(description = "Incoming vs Outgoing", requiredMode = Schema.RequiredMode.REQUIRED) + private ContractAgreementDirection direction; + + @Schema(description = "Other Connector's Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String counterPartyConnectorEndpoint; + + @Schema(description = "Other Connector's Participant ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String counterPartyParticipantId; + + @Schema(description = "Asset Name", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetName; + + @Schema(description = "Asset ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetId; + + @Schema(description = "Error Message") + private String errorMessage; + +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java new file mode 100644 index 000000000..cfe921976 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class TransferHistoryPage { + @Schema(description = "Transfer History Page Entries", requiredMode = Schema.RequiredMode.REQUIRED) + private List transferEntries; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessSimplifiedState.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessSimplifiedState.java new file mode 100644 index 000000000..a67edd49d --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessSimplifiedState.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * This class exists because we want to highlight either running or failed transfer processes in our UI. + *

      + * That distinction has to be made somewhere. Let's rather do that distinction in the backend. + */ +@Schema(description = "Simplified Transfer Process State to be used in UI", enumAsRef = true) +public enum TransferProcessSimplifiedState { + RUNNING, + OK, + ERROR +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java new file mode 100644 index 000000000..210483a3d --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Transfer Process State interpreted") +public class TransferProcessState { + @Schema(description = "State name or 'CUSTOM'. State names only exist for original EDC Transfer Process States.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + @Schema(description = "State code", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer code; + @Schema(description = "Whether we are running, in an error state or done.", requiredMode = Schema.RequiredMode.REQUIRED) + private TransferProcessSimplifiedState simplifiedState; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java new file mode 100644 index 000000000..339425b1c --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java @@ -0,0 +1,46 @@ + +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Contract Negotiation Information") +public class UiContractNegotiation { + + @Schema(description = "Contract Negotiation Id", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractNegotiationId; + + @Schema(description = "Contract Negotiation Creation Time", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Contract Agreement Id") + private String contractAgreementId; + + @Schema(description = "State of the Contract Negotiation state machine", requiredMode = Schema.RequiredMode.REQUIRED) + private ContractNegotiationState state; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java new file mode 100644 index 000000000..9be8ba42d --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "Catalog Data Offer's Contract Offer as required by the UI") +public class UiContractOffer { + @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractOfferId; + + @Schema(description = "Policy", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicy policy; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java new file mode 100644 index 000000000..465e4a300 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Contract Definition Criterion as supported by the UI") +public class UiCriterion { + @Schema(description = "Left Operand", requiredMode = Schema.RequiredMode.REQUIRED) + private String operandLeft; + + @Schema(description = "Operator", requiredMode = Schema.RequiredMode.REQUIRED) + private UiCriterionOperator operator; + + @Schema(description = "Right Operand", requiredMode = Schema.RequiredMode.REQUIRED) + private UiCriterionLiteral operandRight; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java new file mode 100644 index 000000000..2209a072b --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java @@ -0,0 +1,36 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +@Schema(description = "Criterion Literal") +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class UiCriterionLiteral { + + private UiCriterionLiteralType type; + + @Schema(description = "Only for type VALUE. The single value representation.") + private String value; + + @Schema(description = "Only for type VALUE_LIST. List of values, e.g. for the IN-Operator.") + private List valueList; + + public static UiCriterionLiteral ofValue(@NonNull String value) { + return new UiCriterionLiteral(UiCriterionLiteralType.VALUE, value, null); + } + + public static UiCriterionLiteral ofValueList(@NonNull List valueList) { + return new UiCriterionLiteral(UiCriterionLiteralType.VALUE_LIST, null, valueList); + } +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteralType.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteralType.java new file mode 100644 index 000000000..f6ce8ec07 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteralType.java @@ -0,0 +1,8 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Value type of an asset selector criterion right expression value", enumAsRef = true) +public enum UiCriterionLiteralType { + VALUE, VALUE_LIST +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java new file mode 100644 index 000000000..6944a3d65 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Contract Definition Criterion + * + * @see org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl + */ +@Getter +@RequiredArgsConstructor +@Schema(description = "Operator for constraints", enumAsRef = true) +public enum UiCriterionOperator { + EQ, + IN, + LIKE; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java new file mode 100644 index 000000000..6f80a6b0b --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "Catalog Data Offer as required by the UI") +public class UiDataOffer { + @Schema(description = "Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String endpoint; + + @Schema(description = "Participant ID. Required for initiating transfers.", requiredMode = Schema.RequiredMode.REQUIRED) + private String participantId; + + @Schema(description = "Asset Information", requiredMode = Schema.RequiredMode.REQUIRED) + private UiAsset asset; + + @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) + private List contractOffers; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java new file mode 100644 index 000000000..9c6dba153 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase; + +import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + + +/** + * Provides the endpoints for use-case specific requests. + */ +@Path("wrapper/use-case-api") +@Tag(name = "Use Case", description = "Generic Use Case Application API Endpoints.") +public interface UseCaseResource { + + @GET + @Path("kpis") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Basic KPIs about the running EDC Connector.") + KpiResult getKpis(); + + @GET + @Path("supported-policy-functions") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "List available functions in policies, prohibitions and obligations.") + List getSupportedFunctions(); +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java new file mode 100644 index 000000000..847f67ee7 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "EDC-status-defining KPIs") +public class KpiResult { + @Schema(description = "Counts of assets", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer assetsCount; + + @Schema(description = "Counts of policies", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer policiesCount; + + @Schema(description = "Counts of contract definitions", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer contractDefinitionsCount; + + @Schema(description = "Counts of contract agreements", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer contractAgreementsCount; + + @Schema(description = "Counts of incoming and outgoing TransferProcesses and status", + requiredMode = Schema.RequiredMode.REQUIRED) + private TransferProcessStatesDto transferProcessDto; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java new file mode 100644 index 000000000..1059db3a5 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Map; + +@Data +@AllArgsConstructor +public class TransferProcessStatesDto { + @Schema(description = "States and count of incoming transferprocess counts", requiredMode = Schema.RequiredMode.REQUIRED) + private Map incomingTransferProcessCounts; + + @Schema(description = "States and counts of outgoing transferprocess counts", requiredMode = Schema.RequiredMode.REQUIRED) + private Map outgoingTransferProcessCounts; +} diff --git a/extensions/wrapper/wrapper-common-api/README.md b/extensions/wrapper/wrapper-common-api/README.md new file mode 100644 index 000000000..8a83c802f --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/README.md @@ -0,0 +1,38 @@ + +
      +

      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Common API Models

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this module + +Common API models between the sovity Community Edition EDC API, sovity Enterprise Edition EDC API and/or the Broker +Server API. + +## Why does this module exist? + +APIs to be implemented outside the wrapper extension itself create their own Gradle Modules, +e.g. [:extensions:wrapper:wrapper-ee-api](../wrapper-ee-api). + +There are few models we can profit from sharing between all APIs. For example, +[`UiPolicy`](src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java), which +contains a supported subset of the original EDC Policy-Entity. We create such a custom policy model +because the core EDC Policy model struggles in OpenAPI Specification YAMLs due to its polymorphism. + +## License + +Apache License 2.0 - see [LICENSE](../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/wrapper-common-api/build.gradle.kts b/extensions/wrapper/wrapper-common-api/build.gradle.kts new file mode 100644 index 000000000..b4856c153 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/build.gradle.kts @@ -0,0 +1,30 @@ +val lombokVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + api("jakarta.validation:jakarta.validation-api:3.0.2") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") + api("jakarta.servlet:jakarta.servlet-api:5.0.0") + + implementation("org.apache.commons:commons-lang3:3.13.0") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java new file mode 100644 index 000000000..95741f6d5 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.Map; + +@Getter +@Setter +@Builder(toBuilder = true) +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Asset Details") +public class AssetDto { + @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetId; + + @Schema(description = "Creation Date of asset", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + + @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) + private Map properties; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java new file mode 100644 index 000000000..9ccdec720 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * Opinionated DTO of an EDC Constraint for permissions. + * + * @author tim.dahlmanns@isst.fraunhofer.de + */ +@Getter +@Setter +@Builder(toBuilder = true) +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = + "Type-Safe OpenAPI generator friendly Constraint DTO that supports an opinionated" + + " subset of the original EDC Constraint Entity.") +public class AtomicConstraintDto { + + @Schema(description = "Left part of the constraint.", + requiredMode = Schema.RequiredMode.REQUIRED) + private String leftExpression; + @Schema(description = "Operator to connect both parts of the constraint.", + requiredMode = Schema.RequiredMode.REQUIRED) + private OperatorDto operator; + @Schema(description = "Right part of the constraint.", + requiredMode = Schema.RequiredMode.REQUIRED) + private String rightExpression; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java new file mode 100644 index 000000000..60437b6f6 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class CriterionDto { + + @NotNull(message = "operandLeft cannot be null") + private Object operandLeft; + @NotNull(message = "operator cannot be null") + private String operator; + private Object operandRight; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java new file mode 100644 index 000000000..17494b6f3 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Expression constraints for policies. + * + * @author tim.dahlmanns@isst.fraunhofer.de + */ +@Getter +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class ExpressionDto { + + @Schema(description = """ + Expression types: + * `EMPTY` - No constraints for the policy + * `ATOMIC_CONSTRAINT` - A single constraint for the policy + * `AND` - Several constraints, all of which must be respected + * `OR` - Several constraints, of which at least one must be respected + * `XOR` - Several constraints, of which exactly one must be respected + """ + ) + private ExpressionType type; + private AtomicConstraintDto atomicConstraint; + private List and; + private List or; + private List xor; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java new file mode 100644 index 000000000..19b9e3e08 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java @@ -0,0 +1,11 @@ +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Sum type enum. + */ +@Schema(enumAsRef = true) +public enum ExpressionType { + EMPTY, ATOMIC_CONSTRAINT, AND, OR, XOR +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java new file mode 100644 index 000000000..34ab6a955 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Equivalent of ODRL Policy Operator for our API Wrapper API. + * + * @author tim.dahlmanns@isst.fraunhofer.de + */ +@Schema(description = "Operator for policies", enumAsRef = true) +public enum OperatorDto { + EQ, + NEQ, + GT, + GEQ, + LT, + LEQ, + IN, + HAS_PART, + IS_A, + IS_ALL_OF, + IS_ANY_OF, + IS_NONE_OF +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java new file mode 100644 index 000000000..4bcd46a9e --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Subset of the possible permissions in the EDC. + * + * @author tim.dahlmanns@isst.fraunhofer.de + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class PermissionDto { + + @Schema(description = "Possible constraints for the permission", + requiredMode = RequiredMode.REQUIRED) + private ExpressionDto constraints; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java new file mode 100644 index 000000000..bc4d074f4 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "Data for creating a Policy Definition") +public class PolicyDefinitionCreateRequest { + @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String policyDefinitionId; + + @Schema(description = "Policy Contents", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicyCreateRequest policy; +} + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java new file mode 100644 index 000000000..b92164a0c --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "Policy Definition as required for the Policy Definition Page") +public class PolicyDefinitionDto { + @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String policyDefinitionId; + + @Schema(description = "Policy Contents", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicy policy; +} + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java new file mode 100644 index 000000000..995b27bf4 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter + +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "Type-Safe Asset Metadata as needed by our UI") +public class UiAsset { + + @Schema(description = "Asset Id", requiredMode = Schema.RequiredMode.REQUIRED) + private String assetId; + + @Schema(description = "Providing Connector's Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + + @Schema(description = "Providing Connector's Participant ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String participantId; + + @Schema(description = "Asset Title", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + @Schema(description = "Asset Organization Name", requiredMode = Schema.RequiredMode.REQUIRED) + private String creatorOrganizationName; + + @Schema(description = "Asset Language", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String language; + + @Schema(description = "Asset Description. Supports markdown.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String description; + + @Schema(description = "Asset Description Short Text generated from description. Contains no markdown.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String descriptionShortText; + + @Schema(description = "Flag that indicates whether this asset is created by this connector.", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean isOwnConnector; + + @Schema(description = "Asset Homepage", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String publisherHomepage; + + @Schema(description = "License URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String licenseUrl; + + @Schema(description = "Version", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String version; + + @Schema(description = "Asset Keywords", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List keywords; + + @Schema(description = "Asset MediaType", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String mediaType; + + @Schema(description = "Homepage URL associated with the Asset", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String landingPageUrl; + + @Schema(description = "HTTP Datasource Hints Proxy Method", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean httpDatasourceHintsProxyMethod; + + @Schema(description = "HTTP Datasource Hints Proxy Path", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean httpDatasourceHintsProxyPath; + + @Schema(description = "HTTP Datasource Hints Proxy Query Params", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean httpDatasourceHintsProxyQueryParams; + + @Schema(description = "HTTP Datasource Hints Proxy Body", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean httpDatasourceHintsProxyBody; + + @Schema(description = "Data Category", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataCategory; + + @Schema(description = "Data Subcategory", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataSubcategory; + + @Schema(description = "Data Model", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataModel; + + @Schema(description = "Geo-Reference Method", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String geoReferenceMethod; + + @Schema(description = "Transport Mode", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String transportMode; + + @Schema(description = "The sovereign is distinct from the publisher by being the legal owner of the data.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String sovereignLegalName; + + @Schema(description = "Geo location", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String geoLocation; + + @Schema(description = "Locations by NUTS standard which divides countries into administrative divisions", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List nutsLocations; + + @Schema(description = "Data sample URLs", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List dataSampleUrls; + + @Schema(description = "Reference file/schema URLs", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List referenceFileUrls; + + @Schema(description = "Additional information on reference files/schemas", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String referenceFilesDescription; + + @Schema(description = "Instructions for use that are not legally relevant e.g. information on how to cite the dataset in papers", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String conditionsForUse; + + @Schema(description = "Data update frequency", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataUpdateFrequency; + + @Schema(description = "Temporal coverage start date", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private LocalDate temporalCoverageFrom; + + @Schema(description = "Temporal coverage end date (inclusive)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private LocalDate temporalCoverageToInclusive; + + @Schema(description = "Contains the entire asset in the JSON-LD format", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String assetJsonLd; + + @Schema(description = "Contains serialized custom properties in the JSON format.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonAsString; + + @Schema(description = "Contains serialized custom properties in the JSON LD format. " + + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonLdAsString; + + @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonAsString; + + @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonLdAsString; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java new file mode 100644 index 000000000..53e6924d6 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Type-Safe OpenAPI generator friendly Asset Create DTO that supports an opinionated subset of the original EDC Asset Entity.") +public class UiAssetCreateRequest { + @Schema(description = "Asset Id", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "Asset Title", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String title; + + @Schema(description = "Asset Language", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String language; + + @Schema(description = "Asset Description", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String description; + + @Schema(description = "Asset Homepage", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String publisherHomepage; + + @Schema(description = "License URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String licenseUrl; + + @Schema(description = "Version", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String version; + + @Schema(description = "Asset Keywords", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List keywords; + + @Schema(description = "Asset MediaType", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String mediaType; + + @Schema(description = "Landing Page URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String landingPageUrl; + + @Schema(description = "Data Category", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataCategory; + + @Schema(description = "Data Subcategory", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataSubcategory; + + @Schema(description = "Data Model", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataModel; + + @Schema(description = "Geo-Reference Method", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String geoReferenceMethod; + + @Schema(description = "Transport Mode", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String transportMode; + + @Schema(description = "The sovereign is distinct from the publisher by being the legal owner of the data.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String sovereignLegalName; + + @Schema(description = "Geo location", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String geoLocation; + + @Schema(description = "Locations by NUTS standard which divides countries into administrative divisions", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List nutsLocations; + + @Schema(description = "Data sample URLs", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List dataSampleUrls; + + @Schema(description = "Reference file/schema URLs", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List referenceFileUrls; + + @Schema(description = "Additional information on reference files/schemas", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String referenceFilesDescription; + + @Schema(description = "Instructions for use that are not legally relevant e.g. information on how to cite the dataset in papers", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String conditionsForUse; + + @Schema(description = "Data update frequency", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataUpdateFrequency; + + @Schema(description = "Temporal coverage start date", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private LocalDate temporalCoverageFrom; + + @Schema(description = "Temporal coverage end date (inclusive)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private LocalDate temporalCoverageToInclusive; + + @Schema(description = "Data Address", requiredMode = Schema.RequiredMode.REQUIRED) + private Map dataAddressProperties; + + @Schema(description = "Contains serialized custom properties in the JSON format.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonAsString; + + @Schema(description = "Contains serialized custom properties in the JSON LD format. " + + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonLdAsString; + + @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonAsString; + + @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonLdAsString; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java new file mode 100644 index 000000000..17c6c54f0 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Data for editing an asset.") +public class UiAssetEditMetadataRequest { + @Schema(description = "Asset Title", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String title; + + @Schema(description = "Asset Language", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String language; + + @Schema(description = "Asset Description", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String description; + + @Schema(description = "Asset Homepage", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String publisherHomepage; + + @Schema(description = "License URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String licenseUrl; + + @Schema(description = "Version", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String version; + + @Schema(description = "Asset Keywords", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List keywords; + + @Schema(description = "Asset MediaType", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String mediaType; + + @Schema(description = "Landing Page URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String landingPageUrl; + + @Schema(description = "Data Category", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataCategory; + + @Schema(description = "Data Subcategory", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataSubcategory; + + @Schema(description = "Data Model", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataModel; + + @Schema(description = "Geo-Reference Method", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String geoReferenceMethod; + + @Schema(description = "Transport Mode", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String transportMode; + + @Schema(description = "The sovereign is distinct from the publisher by being the legal owner of the data.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String sovereignLegalName; + + @Schema(description = "Geo location", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String geoLocation; + + @Schema(description = "Locations by NUTS standard which divides countries into administrative divisions", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List nutsLocations; + + @Schema(description = "Data sample URLs", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List dataSampleUrls; + + @Schema(description = "Reference file/schema URLs", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List referenceFileUrls; + + @Schema(description = "Additional information on reference files/schemas", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String referenceFilesDescription; + + @Schema(description = "Instructions for use that are not legally relevant e.g. information on how to cite the dataset in papers", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String conditionsForUse; + + @Schema(description = "Data update frequency", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String dataUpdateFrequency; + + @Schema(description = "Temporal coverage start date", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private LocalDate temporalCoverageFrom; + + @Schema(description = "Temporal coverage end date (inclusive)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private LocalDate temporalCoverageToInclusive; + + @Schema(description = "Contains serialized custom properties in the JSON format.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonAsString; + + @Schema(description = "Contains serialized custom properties in the JSON LD format. " + + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonLdAsString; + + @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonAsString; + + @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonLdAsString; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java new file mode 100644 index 000000000..ba307faa2 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "Type-Safe OpenAPI generator friendly Policy DTO as needed by our UI") +public class UiPolicy { + @Schema(description = "EDC Policy JSON-LD. This is required because the EDC requires the " + + "full policy when initiating contract negotiations.", requiredMode = RequiredMode.REQUIRED) + private String policyJsonLd; + + @Schema(description = "Conjunction of required expressions for the policy to evaluate to TRUE.") + private List constraints; + + @Schema(description = "When trying to reduce the policy JSON-LD to our opinionated subset of functionalities, " + + "many fields and functionalities are unsupported. Should any discrepancies occur during " + + "the mapping process, we'll collect them here.", requiredMode = RequiredMode.REQUIRED) + private List errors; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java new file mode 100644 index 000000000..07c38e6fd --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "ODRL AtomicConstraint as supported by our UI") +public class UiPolicyConstraint { + @Schema(description = "Left side of the expression.", requiredMode = RequiredMode.REQUIRED) + private String left; + + @Schema(description = "Operator, e.g. EQ", requiredMode = RequiredMode.REQUIRED) + private OperatorDto operator; + + @Schema(description = "Right side of the expression", requiredMode = RequiredMode.REQUIRED) + private UiPolicyLiteral right; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java new file mode 100644 index 000000000..769f615a2 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "Type-Safe OpenAPI generator friendly Policy Create DTO that supports an opinionated" + + " subset of the original EDC Policy Entity.") +public class UiPolicyCreateRequest { + @Schema(description = "Conjunction of required expressions for the policy to evaluate to TRUE.") + private List constraints; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java new file mode 100644 index 000000000..355c8e1c3 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Sum type: A String, a list of Strings or a generic JSON value.") +public class UiPolicyLiteral { + @Schema(description = "Value Type", requiredMode = RequiredMode.REQUIRED) + private UiPolicyLiteralType type; + + @Schema(description = "Only for types STRING and JSON") + private String value; + + @Schema(description = "Only for type STRING_LIST") + private List valueList; + + public static UiPolicyLiteral ofString(String string) { + return UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(string) + .build(); + } + + public static UiPolicyLiteral ofJson(String jsonString) { + return UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.JSON) + .value(jsonString) + .build(); + } + + public static UiPolicyLiteral ofStringList(Collection strings) { + return UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING_LIST) + .valueList(new ArrayList<>(strings)) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteralType.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteralType.java new file mode 100644 index 000000000..1268af493 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteralType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Supported Types of values for the right hand side of an expression", enumAsRef = true) +public enum UiPolicyLiteralType { + STRING, + STRING_LIST, + JSON +} diff --git a/extensions/wrapper/wrapper-common-mappers/README.md b/extensions/wrapper/wrapper-common-mappers/README.md new file mode 100644 index 000000000..15e50920e --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/README.md @@ -0,0 +1,34 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Common API Model Mappers

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this module + +Common API models naooers between the sovity Community Edition EDC API, sovity Enterprise Edition EDC API and/or the +Broker +Server API. + +## Why does this module exist? + +Our common API models defined in [wrapper-common-api](../wrapper-common-api) need mapping to and from the core EDC +types in both the EDC CE, EDC EE and Broker Server. + +## License + +Apache License 2.0 - see [LICENSE](../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts new file mode 100644 index 000000000..47a6496c8 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -0,0 +1,50 @@ +val lombokVersion: String by project + +val assertj: String by project +val edcGroup: String by project +val edcVersion: String by project +val jsonUnit: String by project +val mockitoVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api("${edcGroup}:policy-model:${edcVersion}") + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:transform-core:${edcVersion}") + api("${edcGroup}:transform-spi:${edcVersion}") + api(project(":extensions:wrapper:wrapper-common-api")) + api(project(":utils:json-and-jsonld-utils")) + implementation("org.apache.commons:commons-lang3:3.13.0") + implementation("org.apache.commons:commons-collections4:4.4") + implementation("com.vladsch.flexmark:flexmark-all:0.64.8") + + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testImplementation(project(":utils:test-utils")) + testImplementation("${edcGroup}:json-ld:${edcVersion}") + testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.mockito:mockito-inline:${mockitoVersion}") + testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java new file mode 100644 index 000000000..1c116d579 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java @@ -0,0 +1,67 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.util.Optional; + + +@RequiredArgsConstructor +public class AssetMapper { + private final TypeTransformerRegistry typeTransformerRegistry; + private final UiAssetMapper uiAssetMapper; + private final JsonLd jsonLd; + + public UiAsset buildUiAsset(Asset asset, String connectorEndpoint, String participantId) { + var assetJsonLd = buildAssetJsonLd(asset); + return buildUiAsset(assetJsonLd, connectorEndpoint, participantId); + } + + public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, String participantId) { + return uiAssetMapper.buildUiAsset(assetJsonLd, connectorEndpoint, participantId); + } + + public Asset buildAsset(UiAssetCreateRequest createRequest, String organizationName) { + var assetJsonLd = uiAssetMapper.buildAssetJsonLd(createRequest, organizationName); + return buildAsset(assetJsonLd); + } + + public Asset buildAssetFromDatasetProperties(JsonObject json) { + return buildAsset(buildAssetJsonLdFromDatasetProperties(json)); + } + + public Asset buildAsset(JsonObject assetJsonLd) { + var expanded = jsonLd.expand(assetJsonLd) + .orElseThrow(FailedMappingException::ofFailure); + return typeTransformerRegistry.transform(expanded, Asset.class) + .orElseThrow(FailedMappingException::ofFailure); + } + + private JsonObject buildAssetJsonLd(Asset asset) { + var assetJsonLd = typeTransformerRegistry.transform(asset, JsonObject.class) + .orElseThrow(FailedMappingException::ofFailure); + return jsonLd.expand(assetJsonLd) + .orElseThrow(FailedMappingException::ofFailure); + } + + public JsonObject buildAssetJsonLdFromDatasetProperties(JsonObject json) { + // Try to use the EDC Prop ID, but if it's not available, fall back to the "@id" property + var assetId = Optional.ofNullable(JsonLdUtils.string(json, Prop.Edc.ID)) + .orElseGet(() -> JsonLdUtils.string(json, Prop.ID)); + + return Json.createObjectBuilder() + .add(Prop.ID, assetId) + .add(Prop.Edc.PROPERTIES, json) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java new file mode 100644 index 000000000..19d992a48 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.model.Operator; + +@RequiredArgsConstructor +public class OperatorMapper { + public OperatorDto getOperatorDto(String operator) { + return OperatorDto.valueOf(operator.toUpperCase()); + } + + public OperatorDto getOperatorDto(Operator operator) { + return OperatorDto.valueOf(operator.name()); + } + + public Operator getOperator(OperatorDto operatorDto) { + return Operator.valueOf(operatorDto.name()); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java new file mode 100644 index 000000000..06da1725f --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java @@ -0,0 +1,112 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import de.sovity.edc.utils.JsonUtils; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.util.ArrayList; + +import static de.sovity.edc.utils.JsonUtils.toJson; + +@RequiredArgsConstructor +public class PolicyMapper { + private final ConstraintExtractor constraintExtractor; + private final AtomicConstraintMapper atomicConstraintMapper; + private final TypeTransformerRegistry typeTransformerRegistry; + + /** + * Builds a simplified UI Policy Model from an ODRL Policy. + *

      + * This operation is lossy. + * + * @param policy ODRL policy + * @return ui policy + */ + public UiPolicy buildUiPolicy(Policy policy) { + MappingErrors errors = MappingErrors.root(); + + var constraints = constraintExtractor.getPermissionConstraints(policy, errors); + + return UiPolicy.builder() + .policyJsonLd(toJson(buildPolicyJsonLd(policy))) + .constraints(constraints) + .errors(errors.getErrors()) + .build(); + } + + /** + * Builds an ODRL Policy from our simplified UI Policy Model. + *

      + * This operation is lossless. + * + * @param policyCreateDto policy + * @return ODRL policy + */ + public Policy buildPolicy(UiPolicyCreateRequest policyCreateDto) { + var constraints = new ArrayList(atomicConstraintMapper.buildAtomicConstraints( + policyCreateDto.getConstraints())); + + var action = Action.Builder.newInstance().type(PolicyValidator.ALLOWED_ACTION).build(); + + var permission = Permission.Builder.newInstance() + .action(action) + .constraints(constraints) + .build(); + + return Policy.Builder.newInstance() + .type(PolicyType.SET) + .permission(permission) + .build(); + } + + /** + * Maps an ODRL Policy from JSON-LD to the Core EDC Type. + *

      + * This operation is lossless. + * + * @param policyJsonLd policy JSON-LD + * @return {@link Policy} + */ + public Policy buildPolicy(JsonObject policyJsonLd) { + return typeTransformerRegistry.transform(policyJsonLd, Policy.class) + .orElseThrow(FailedMappingException::ofFailure); + } + + /** + * Maps an ODRL Policy from JSON-LD to the Core EDC Type. + *

      + * This operation is lossless. + * + * @param policyJsonLd policy JSON-LD + * @return {@link Policy} + */ + public Policy buildPolicy(String policyJsonLd) { + return buildPolicy(JsonUtils.parseJsonObj(policyJsonLd)); + } + + /** + * Maps an ODRL Policy from the Core EDC Type to the JSON-LD. + *

      + * This operation is lossless. + * + * @param policy {@link Policy} + * @return policy JSON-LD + */ + public JsonObject buildPolicyJsonLd(Policy policy) { + return typeTransformerRegistry.transform(policy, JsonObject.class) + .orElseThrow(FailedMappingException::ofFailure); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java new file mode 100644 index 000000000..499e2b0ab --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +@RequiredArgsConstructor +public class AssetJsonLdUtils { + + public String getId(JsonObject assetJsonLd) { + return JsonLdUtils.string(assetJsonLd, Prop.ID); + } + + public String getTitle(JsonObject assetJsonLd) { + var properties = JsonLdUtils.object(assetJsonLd, Prop.Edc.PROPERTIES); + var title = JsonLdUtils.string(properties, Prop.Dcterms.TITLE); + return isBlank(title) ? getId(assetJsonLd) : title; + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java new file mode 100644 index 000000000..c39a26de4 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.LiteralExpression; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class AtomicConstraintMapper { + private final LiteralMapper literalMapper; + private final OperatorMapper operatorMapper; + + /** + * Create ODRL {@link AtomicConstraint}s from {@link UiPolicyConstraint}s + *

      + * This operation is lossless. + * + * @param constraints ui constraints + * @return ODRL constraints + */ + public List buildAtomicConstraints(List constraints) { + if (constraints == null) { + return List.of(); + } + + return constraints.stream() + .map(this::buildAtomicConstraint) + .toList(); + } + + /** + * Create {@link UiPolicyConstraint} from ODRL {@link AtomicConstraint} + *

      + * This operation is lossy. + * + * @param atomicConstraint atomic contraints + * @param errors errors + * @return ui policy constraint + */ + public Optional buildUiConstraint( + @NonNull AtomicConstraint atomicConstraint, + MappingErrors errors + ) { + var leftValue = literalMapper.getExpressionString(atomicConstraint.getLeftExpression(), + errors.forChildObject("leftExpression")); + + var operator = getOperator(atomicConstraint, errors); + + var rightValue = literalMapper.getExpressionValue(atomicConstraint.getRightExpression(), + errors.forChildObject("rightExpression")); + + if (leftValue.isEmpty() || rightValue.isEmpty() || operator.isEmpty()) { + return Optional.empty(); + } + + UiPolicyConstraint result = UiPolicyConstraint.builder() + .left(leftValue.get()) + .operator(operator.get()) + .right(rightValue.get()) + .build(); + + return Optional.of(result); + } + + private Optional getOperator(AtomicConstraint atomicConstraint, MappingErrors errors) { + var operator = atomicConstraint.getOperator(); + + if (operator == null) { + errors.forChildObject("operator").add("Operator is null."); + return Optional.empty(); + } + + return Optional.of(operatorMapper.getOperatorDto(operator)); + } + + private AtomicConstraint buildAtomicConstraint(UiPolicyConstraint constraint) { + var left = constraint.getLeft(); + var operator = operatorMapper.getOperator(constraint.getOperator()); + var right = literalMapper.getUiLiteralValue(constraint.getRight()); + + return AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression(left)) + .operator(operator) + .rightExpression(new LiteralExpression(right)) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java new file mode 100644 index 000000000..5ea4ab12e --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.model.AndConstraint; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.OrConstraint; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.XoneConstraint; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class ConstraintExtractor { + private final PolicyValidator policyValidator; + private final AtomicConstraintMapper atomicConstraintMapper; + + /** + * Build {@link UiPolicyConstraint}s from an ODRL {@link Policy}. + *

      + * This operation is lossy which is why we document warnings / errors in {@link MappingErrors}. + * + * @param policy ODRL policy + * @param errors mapping errors + * @return ui policy constraints + */ + public List getPermissionConstraints(Policy policy, MappingErrors errors) { + policyValidator.validateOtherPolicyFieldsUnset(policy, errors); + + var permissions = policy.getPermissions(); + if (permissions == null) { + return List.of(); + } + + + List constraints = new ArrayList<>(); + for (int iPermission = 0; iPermission < permissions.size(); iPermission++) { + var permissionErrors = errors.forChildObject("permissions").forChildArrayElement(iPermission); + var permission = permissions.get(iPermission); + constraints.addAll(getPermissionConstraints(permission, permissionErrors)); + } + return constraints; + } + + private List getPermissionConstraints(Permission permission, MappingErrors errors) { + policyValidator.validateOtherPermissionFieldsUnset(permission, errors); + + if (permission == null) { + return List.of(); + } + + var constraints = permission.getConstraints(); + if (constraints == null) { + return List.of(); + } + + var constraintsMapped = new ArrayList(); + for (int i = 0; i < constraints.size(); i++) { + var constraintErrors = errors.forChildObject("constraints").forChildArrayElement(i); + var constraint = constraints.get(i); + + var constraintMapped = buildConstraint(constraint, constraintErrors); + constraintMapped.ifPresent(constraintsMapped::add); + } + return constraintsMapped; + } + + private Optional buildConstraint(Constraint constraint, MappingErrors errors) { + if (constraint == null) { + errors.add("Constraint is null."); + return Optional.empty(); + } + + if (constraint instanceof XoneConstraint) { + errors.add("XoneConstraints are currently unsupported."); + return Optional.empty(); + } + + if (constraint instanceof AndConstraint) { + errors.add("AndConstraints are currently unsupported."); + return Optional.empty(); + } + + if (constraint instanceof OrConstraint) { + errors.add("OrConstraints are currently unsupported."); + return Optional.empty(); + } + + if (!(constraint instanceof AtomicConstraint)) { + errors.add("Unknown constraint type %s.".formatted(constraint.getClass().getName())); + return Optional.empty(); + } + + return atomicConstraintMapper.buildUiConstraint((AtomicConstraint) constraint, errors); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java new file mode 100644 index 000000000..5af531863 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.types.domain.DataAddress; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +public class EdcPropertyUtils { + + /** + * Converts a {@code Map} to {@code Map}. + *

      + * Our API forsakes asset properties that are complex objects / JSON and only keeps string + * properties. + * + * @param map all properties + * @return string properties + */ + public Map truncateToMapOfString(Map map) { + Map result = new HashMap<>(); + + if (map == null) { + return result; + } + + for (Map.Entry entry : map.entrySet()) { + Object value = entry.getValue(); + + String valueString; + if (value == null) { + valueString = null; + } else if (value instanceof String str) { + valueString = str; + } else if (value instanceof Double) { + valueString = String.valueOf(value); + } else if (value instanceof Integer) { + valueString = String.valueOf(value); + } else { + continue; + } + + result.put(entry.getKey(), valueString); + } + return result; + } + + @SuppressWarnings({"unchecked", "rawtypes", "java:S1905"}) + public Map toMapOfObject(Map map) { + if (map == null) { + return Map.of(); + } + return new HashMap<>((Map) (Map) map); + } + + public DataAddress buildDataAddress(Map properties) { + return DataAddress.Builder.newInstance() + .properties(toMapOfObject(properties)) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java new file mode 100644 index 000000000..85c7b0e7f --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import org.eclipse.edc.spi.result.Failure; + +public class FailedMappingException extends RuntimeException { + public FailedMappingException(String message) { + super(message); + } + + public static FailedMappingException ofFailure(Failure failure) { + return new FailedMappingException(failure.getFailureDetail()); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java new file mode 100644 index 000000000..5604b2dfe --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.utils.JsonUtils; +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; + +import java.time.LocalDate; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonBuilderUtils { + + protected static JsonObjectBuilder addNonNull(JsonObjectBuilder builder, String key, String value) { + if (value != null) { + builder.add(key, value); + } + return builder; + } + + protected static JsonObjectBuilder addNotBlank(JsonObjectBuilder builder, String key, String value) { + if (value != null && !value.trim().isBlank()) { + builder.add(key, value); + } + return builder; + } + + protected static JsonObjectBuilder addNonNull(JsonObjectBuilder builder, String key, LocalDate value) { + if (value != null) { + builder.add(key, value.toString()); + } + return builder; + } + + protected static JsonObjectBuilder addNonNullArray(JsonObjectBuilder builder, String key, List values) { + if (CollectionUtils.isNotEmpty(values)) { + builder.add(key, Json.createArrayBuilder(values)); + } + return builder; + } + + protected static JsonObjectBuilder addNonNullJsonValue(JsonObjectBuilder builder, String key, String jsonString) { + if (jsonString == null) { + return builder; + } + var value = JsonUtils.parseJsonValue(jsonString); + if (value.getValueType() == JsonValue.ValueType.NULL) { + return builder; + } + + builder.add(key, value); + return builder; + } + + protected static JsonObjectBuilder addNonNullJsonValue(JsonObjectBuilder builder, String key, JsonValue value) { + if (value == null || value.getValueType() == JsonValue.ValueType.NULL) { + return builder; + } + + builder.add(key, value); + return builder; + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java new file mode 100644 index 000000000..2bf607606 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.edc.policy.model.Expression; +import org.eclipse.edc.policy.model.LiteralExpression; + +import java.util.Collection; +import java.util.Optional; + +@RequiredArgsConstructor +public class LiteralMapper { + private final ObjectMapper jsonLdObjectMapper; + + @SneakyThrows + public Object getUiLiteralValue(UiPolicyLiteral literal) { + if (literal == null) { + return null; + } + + return switch (literal.getType()) { + case STRING -> literal.getValue(); + case STRING_LIST -> literal.getValueList(); + case JSON -> jsonLdObjectMapper.readValue(literal.getValue(), Object.class); + }; + } + + public Optional getExpressionString( + Expression expression, + MappingErrors errors + ) { + return getLiteralExpression(expression, errors).flatMap(literalExpression -> + getLiteralExpressionString(literalExpression, errors)); + } + + public Optional getExpressionValue( + Expression expression, + MappingErrors errors + ) { + return getLiteralExpression(expression, errors).flatMap(this::getLiteralExpressionValue); + } + + private Optional getLiteralExpressionString( + LiteralExpression literalExpression, + MappingErrors errors + ) { + var value = literalExpression.getValue(); + if (value == null) { + errors.forChildObject("value").add("Is not a string, but null."); + return Optional.empty(); + } + + if (!(value instanceof String)) { + errors.forChildObject("value").add("Is not a string."); + return Optional.empty(); + } + + return Optional.of((String) value); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + private Optional getLiteralExpressionValue(LiteralExpression literalExpression) { + Object value = literalExpression.getValue(); + boolean isString = value instanceof String; + if (isString) { + return Optional.of(UiPolicyLiteral.ofString((String) value)); + } + + boolean isStringList = value instanceof Collection && ((Collection) value).stream() + .allMatch(it -> it == null || it instanceof String); + if (isStringList) { + return Optional.of(UiPolicyLiteral.ofStringList((Collection) value)); + } + + String json = jsonLdObjectMapper.writeValueAsString(value); + return Optional.of(UiPolicyLiteral.ofJson(json)); + } + + + Optional getLiteralExpression(Expression expression, MappingErrors errors) { + if (expression == null) { + errors.add("Expression is null."); + return Optional.empty(); + } + + if (!(expression instanceof LiteralExpression)) { + errors.add("Expression type is not LiteralExpression, but %s.".formatted(expression.getClass().getName())); + return Optional.empty(); + } + + return Optional.of((LiteralExpression) expression); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java new file mode 100644 index 000000000..e6894173a --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Trying to reduce an ODRL {@link org.eclipse.edc.policy.model.Policy} to a + * {@link UiPolicy} is a lossful operation. + *

      + * During the mapping errors can occur, as parts of the ODRL Policy aren't supported. + *

      + * This class helps to collect those errors. + */ +@RequiredArgsConstructor +public class MappingErrors { + @Getter + private final List errors; + private final String path; + + public static MappingErrors root() { + return new MappingErrors(new ArrayList<>(), "$"); + } + + public void add(String message) { + errors.add("%s: %s".formatted(path, message)); + } + + public MappingErrors forChildObject(String name) { + return new MappingErrors(errors, "%s.%s".formatted(path, name)); + } + + public MappingErrors forChildArrayElement(int index) { + return new MappingErrors(errors, "%s[%d]".formatted(path, index)); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java new file mode 100644 index 000000000..65b7e4b49 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.MutableDataSet; +import org.jsoup.Jsoup; + +public class MarkdownToTextConverter { + public String extractText(String markdown) { + var options = new MutableDataSet(); + var parser = Parser.builder(options).build(); + var renderer = HtmlRenderer.builder(options).build(); + var document = parser.parse(markdown); + var html = renderer.render(document); + return Jsoup.parse(html).text(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java new file mode 100644 index 000000000..a60f61b2b --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +@FunctionalInterface +public interface OwnConnectorEndpointService { + boolean isOwnConnectorEndpoint(String endpoint); +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java new file mode 100644 index 000000000..70e558338 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import lombok.val; +import org.eclipse.edc.spi.types.domain.DataAddress; + +import java.util.HashMap; +import java.util.Map; + +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; + +public class ParameterizationCompatibilityUtils { + private static final String WORKAROUND = "https://sovity.de/workaround/proxy/param/"; + + private static final Map MAPPINGS = Map.of( + + EDC_NAMESPACE + "method", WORKAROUND + "method", + EDC_NAMESPACE + "pathSegments", WORKAROUND + "pathSegments", + EDC_NAMESPACE + "queryParams", WORKAROUND + "queryParams", + EDC_NAMESPACE + "body", WORKAROUND + "body", + EDC_NAMESPACE + "mediaType", WORKAROUND + "mediaType", + EDC_NAMESPACE + "contentType", WORKAROUND + "mediaType" + ); + + public DataAddress enrich(DataAddress dataAddress, Map transferProcessProperties) { + if(transferProcessProperties == null) { + return dataAddress; + } + + + HashMap combined = new HashMap<>(dataAddress.getProperties()); + + for (val property : transferProcessProperties.entrySet()) { + val workaroundProperty = MAPPINGS.get(property.getKey()); + if (workaroundProperty != null) { + combined.put(workaroundProperty, property.getValue()); + } + } + + return DataAddress.Builder.newInstance() + .properties(combined) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java new file mode 100644 index 000000000..131470fcb --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; + +import static org.apache.commons.collections4.CollectionUtils.isEmpty; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@RequiredArgsConstructor +public class PolicyValidator { + + public static final String ALLOWED_ACTION = "USE"; + + public void validateOtherPolicyFieldsUnset(Policy policy, MappingErrors errors) { + if (policy == null) { + errors.add("Policy is null"); + return; + } + + if (isEmpty(policy.getPermissions())) { + errors.add("Policy has no permissions."); + } + + if (isNotEmpty(policy.getProhibitions())) { + errors.add("Policy has prohibitions, which are currently unsupported."); + } + + if (isNotEmpty(policy.getObligations())) { + errors.add("Policy has obligations, which are currently unsupported."); + } + + if (StringUtils.isNotBlank(policy.getInheritsFrom())) { + errors.add("Policy has inheritsFrom, which is currently unsupported."); + } + + if (StringUtils.isNotBlank(policy.getAssigner())) { + errors.add("Policy has an assigner, which is currently unsupported."); + } + + if (StringUtils.isNotBlank(policy.getAssignee())) { + errors.add("Policy has an assignee, which is currently unsupported."); + } + + if (policy.getExtensibleProperties() != null && !policy.getExtensibleProperties().isEmpty()) { + errors.add("Policy has extensible properties."); + } + + if (policy.getType() != PolicyType.SET) { + errors.add("Policy does not have type SET, but %s, which is currently unsupported.".formatted(policy.getType())); + } + } + + public void validateOtherPermissionFieldsUnset(Permission permission, MappingErrors errors) { + if (permission == null) { + errors.add("Permission is null."); + return; + } + + if (CollectionUtils.isEmpty(permission.getConstraints())) { + errors.add("Permission has no constraints."); + } + + if (CollectionUtils.isNotEmpty(permission.getDuties())) { + errors.add("Permission has duties, which is currently unsupported."); + } + + if (isNotBlank(permission.getAssigner())) { + errors.add("Permission has an assigner, which is currently unsupported."); + } + + if (isNotBlank(permission.getAssignee())) { + errors.add("Permission has an assignee, which is currently unsupported."); + } + + validateAction(permission.getAction(), errors.forChildObject("action")); + } + + private void validateAction(Action action, MappingErrors errors) { + if (action == null) { + errors.add("Action is null."); + return; + } + + if (!ALLOWED_ACTION.equals(action.getType())) { + errors.add("Action has a type that is not '%s', but '%s'.".formatted(ALLOWED_ACTION, action.getType())); + } + + if (StringUtils.isNotBlank(action.getIncludedIn())) { + errors.add("Action has a value for includedIn, which is currently unsupported."); + } + + if (action.getConstraint() != null) { + errors.add("Action has a constraint, which is currently unsupported."); + } + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java new file mode 100644 index 000000000..5de523e29 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +public class TextUtils { + public String abbreviate(String text, int maxCharacters) { + if (text == null) { + return null; + } + return text.substring(0, Math.min(maxCharacters, text.length())); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java new file mode 100644 index 000000000..523acd351 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import de.sovity.edc.utils.jsonld.vocab.Prop.SovityDcatExt.HttpDatasourceHints; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.val; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNonNull; +import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNonNullArray; +import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNonNullJsonValue; +import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNotBlank; +import static org.apache.commons.lang3.StringUtils.isBlank; + +@RequiredArgsConstructor +public class UiAssetMapper { + private final EdcPropertyUtils edcPropertyUtils; + private final AssetJsonLdUtils assetJsonLdUtils; + private final MarkdownToTextConverter markdownToTextConverter; + private final TextUtils textUtils; + private final OwnConnectorEndpointService ownConnectorEndpointService; + + public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, String participantId) { + var properties = JsonLdUtils.object(assetJsonLd, Prop.Edc.PROPERTIES); + + var uiAsset = new UiAsset(); + uiAsset.setAssetJsonLd(JsonUtils.toJson(JsonLdUtils.tryCompact(assetJsonLd))); + + var id = assetJsonLdUtils.getId(assetJsonLd); + var title = assetJsonLdUtils.getTitle(assetJsonLd); + + var distribution = JsonLdUtils.object(properties, Prop.Dcat.DISTRIBUTION); + uiAsset.setMediaType(JsonLdUtils.string(distribution, Prop.Dcat.MEDIATYPE)); + uiAsset.setDataSampleUrls(JsonLdUtils.stringList(distribution, Prop.Adms.SAMPLE)); + var rights = JsonLdUtils.object(distribution, Prop.Dcterms.RIGHTS); + uiAsset.setConditionsForUse(JsonLdUtils.string(rights, Prop.Rdfs.LABEL)); + var mobilityDataStandard = JsonLdUtils.object(distribution, Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); + uiAsset.setDataModel(JsonLdUtils.string(mobilityDataStandard, Prop.ID)); + var referenceFiles = JsonLdUtils.object(mobilityDataStandard, Prop.MobilityDcatAp.SCHEMA); + uiAsset.setReferenceFileUrls(JsonLdUtils.stringList(referenceFiles, Prop.Dcat.DOWNLOAD_URL)); + uiAsset.setReferenceFilesDescription(JsonLdUtils.string(referenceFiles, Prop.Rdfs.LITERAL)); + + + var temporalCoverage = JsonLdUtils.object(properties, Prop.Dcterms.TEMPORAL); + uiAsset.setTemporalCoverageFrom(JsonLdUtils.localDate(temporalCoverage, Prop.Dcat.START_DATE)); + uiAsset.setTemporalCoverageToInclusive(JsonLdUtils.localDate(temporalCoverage, Prop.Dcat.END_DATE)); + + var spatial = JsonLdUtils.object(properties, Prop.Dcterms.SPATIAL); + uiAsset.setGeoLocation(JsonLdUtils.string(spatial, Prop.Skos.PREF_LABEL)); + uiAsset.setNutsLocations(JsonLdUtils.stringList(spatial, Prop.Dcterms.IDENTIFIER)); + + var mobilityTheme = JsonLdUtils.object(properties, Prop.MobilityDcatAp.MOBILITY_THEME); + uiAsset.setDataCategory(JsonLdUtils.string(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_CATEGORY)); + uiAsset.setDataSubcategory(JsonLdUtils.string(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_SUBCATEGORY)); + + var creator = JsonLdUtils.object(properties, Prop.Dcterms.CREATOR); + var creatorOrganizationName = JsonLdUtils.string(creator, Prop.Foaf.NAME); + creatorOrganizationName = isBlank(creatorOrganizationName) ? participantId : creatorOrganizationName; + + + var description = JsonLdUtils.string(properties, Prop.Dcterms.DESCRIPTION); + uiAsset.setAssetId(id); + uiAsset.setConnectorEndpoint(connectorEndpoint); + uiAsset.setParticipantId(participantId); + uiAsset.setTitle(title); + uiAsset.setLicenseUrl(JsonLdUtils.string(properties, Prop.Dcterms.LICENSE)); + uiAsset.setDescription(description); + uiAsset.setDescriptionShortText(buildShortDescription(description)); + uiAsset.setIsOwnConnector(ownConnectorEndpointService.isOwnConnectorEndpoint(connectorEndpoint)); + uiAsset.setLanguage(JsonLdUtils.string(properties, Prop.Dcterms.LANGUAGE)); + uiAsset.setVersion(JsonLdUtils.string(properties, Prop.Dcat.VERSION)); + uiAsset.setLandingPageUrl(JsonLdUtils.string(properties, Prop.Dcat.LANDING_PAGE)); + uiAsset.setGeoReferenceMethod(JsonLdUtils.string(properties, Prop.MobilityDcatAp.GEO_REFERENCE_METHOD)); + uiAsset.setTransportMode(JsonLdUtils.string(properties, Prop.MobilityDcatAp.TRANSPORT_MODE)); + uiAsset.setSovereignLegalName(JsonLdUtils.string(properties, Prop.Dcterms.RIGHTS_HOLDER)); + uiAsset.setDataUpdateFrequency(JsonLdUtils.string(properties, Prop.Dcterms.ACCRUAL_PERIODICITY)); + uiAsset.setKeywords(JsonLdUtils.stringList(properties, Prop.Dcat.KEYWORDS)); + + uiAsset.setHttpDatasourceHintsProxyMethod(JsonLdUtils.bool(properties, HttpDatasourceHints.METHOD)); + uiAsset.setHttpDatasourceHintsProxyPath(JsonLdUtils.bool(properties, HttpDatasourceHints.PATH)); + uiAsset.setHttpDatasourceHintsProxyQueryParams(JsonLdUtils.bool(properties, HttpDatasourceHints.QUERY_PARAMS)); + uiAsset.setHttpDatasourceHintsProxyBody(JsonLdUtils.bool(properties, HttpDatasourceHints.BODY)); + + var publisher = JsonLdUtils.object(properties, Prop.Dcterms.PUBLISHER); + uiAsset.setPublisherHomepage(JsonLdUtils.string(publisher, Prop.Foaf.HOMEPAGE)); + + uiAsset.setCustomJsonAsString(JsonLdUtils.string(properties, Prop.SovityDcatExt.CUSTOM_JSON)); + + uiAsset.setCreatorOrganizationName(creatorOrganizationName); + + // Additional / Remaining Properties + // TODO: diff nested objects + val remaining = removeHandledProperties(properties, List.of( + // Implicitly handled / should be skipped if found + Prop.ID, + Prop.TYPE, + Prop.CONTEXT, + Prop.Edc.ID, + Prop.Dcterms.IDENTIFIER, + + // Explicitly handled + Prop.Dcat.DISTRIBUTION, + Prop.Dcat.KEYWORDS, + Prop.Dcat.LANDING_PAGE, + Prop.Dcat.VERSION, + Prop.Dcterms.CREATOR, + Prop.Dcterms.DESCRIPTION, + Prop.Dcterms.LANGUAGE, + Prop.Dcterms.LICENSE, + Prop.Dcterms.PUBLISHER, + Prop.Dcterms.TITLE, + Prop.MobilityDcatAp.GEO_REFERENCE_METHOD, + Prop.MobilityDcatAp.TRANSPORT_MODE, + Prop.Dcterms.TEMPORAL, + Prop.Dcterms.SPATIAL, + Prop.MobilityDcatAp.MOBILITY_THEME, + Prop.Dcterms.RIGHTS_HOLDER, + Prop.Dcterms.ACCRUAL_PERIODICITY, + + HttpDatasourceHints.BODY, + HttpDatasourceHints.METHOD, + HttpDatasourceHints.PATH, + HttpDatasourceHints.QUERY_PARAMS, + + Prop.SovityDcatExt.CUSTOM_JSON + )); + + // custom properties + val serializedJsonLd = packAsJsonLdProperties(remaining); + uiAsset.setCustomJsonLdAsString(serializedJsonLd); + + // Private Properties + val privateProperties = getPrivateProperties(assetJsonLd); + if (privateProperties != null) { + val privateCustomJson = JsonLdUtils.string(privateProperties, Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON); + uiAsset.setPrivateCustomJsonAsString(privateCustomJson); + + val privateRemaining = removeHandledProperties( + privateProperties, + List.of(Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON)); + val privateSerializedJsonLd = packAsJsonLdProperties(privateRemaining); + uiAsset.setPrivateCustomJsonLdAsString(privateSerializedJsonLd); + } + + return uiAsset; + } + + private static String packAsJsonLdProperties(JsonObject remaining) { + val customJsonLd = Json.createObjectBuilder(); + remaining.entrySet().stream() + .filter(it -> !JsonLdUtils.isEmptyArray(it.getValue()) || !JsonLdUtils.isEmptyObject(it.getValue())) + .forEach(it -> customJsonLd.add(it.getKey(), it.getValue())); + val compacted = JsonLdUtils.tryCompact(customJsonLd.build()); + return JsonUtils.toJson(compacted); + } + + @SneakyThrows + @Nullable + public JsonObject buildAssetJsonLd( + UiAssetCreateRequest uiAssetCreateRequest, + String organizationName + ) { + var properties = getAssetProperties(uiAssetCreateRequest, organizationName); + var privateProperties = getAssetPrivateProperties(uiAssetCreateRequest); + var dataAddress = getDataAddress(uiAssetCreateRequest); + + return Json.createObjectBuilder() + .add(Prop.ID, uiAssetCreateRequest.getId()) + .add(Prop.TYPE, Prop.Edc.TYPE_ASSET) + .add(Prop.Edc.PROPERTIES, properties) + .add(Prop.Edc.PRIVATE_PROPERTIES, privateProperties) + .add(Prop.Edc.DATA_ADDRESS, dataAddress) + .build(); + } + + private JsonObjectBuilder getAssetProperties( + UiAssetCreateRequest uiAssetCreateRequest, + String organizationName + ) { + var properties = Json.createObjectBuilder(); + + addNonNull(properties, Prop.Edc.ID, uiAssetCreateRequest.getId()); + addNonNull(properties, Prop.Dcterms.LICENSE, uiAssetCreateRequest.getLicenseUrl()); + addNonNull(properties, Prop.Dcterms.TITLE, uiAssetCreateRequest.getTitle()); + addNonNull(properties, Prop.Dcterms.DESCRIPTION, uiAssetCreateRequest.getDescription()); + addNonNull(properties, Prop.Dcterms.LANGUAGE, uiAssetCreateRequest.getLanguage()); + addNonNull(properties, Prop.Dcat.VERSION, uiAssetCreateRequest.getVersion()); + addNonNull(properties, Prop.Dcat.LANDING_PAGE, uiAssetCreateRequest.getLandingPageUrl()); + addNonNull(properties, Prop.MobilityDcatAp.GEO_REFERENCE_METHOD, uiAssetCreateRequest.getGeoReferenceMethod()); + addNonNull(properties, Prop.MobilityDcatAp.TRANSPORT_MODE, uiAssetCreateRequest.getTransportMode()); + addNonNull(properties, Prop.Dcterms.RIGHTS_HOLDER, uiAssetCreateRequest.getSovereignLegalName()); + addNonNull(properties, Prop.Dcterms.ACCRUAL_PERIODICITY, uiAssetCreateRequest.getDataUpdateFrequency()); + + addNonNullArray(properties, Prop.Dcat.KEYWORDS, uiAssetCreateRequest.getKeywords()); + + if (uiAssetCreateRequest.getPublisherHomepage() != null) { + properties.add(Prop.Dcterms.PUBLISHER, Json.createObjectBuilder() + .add(Prop.Foaf.HOMEPAGE, uiAssetCreateRequest.getPublisherHomepage())); + } + + properties.add(Prop.Dcterms.CREATOR, Json.createObjectBuilder() + .add(Prop.Foaf.NAME, organizationName)); + + var distribution = buildDistribution(uiAssetCreateRequest); + if (distribution != null) { + properties.add(Prop.Dcat.DISTRIBUTION, distribution); + } + + if (uiAssetCreateRequest.getTemporalCoverageFrom() != null || uiAssetCreateRequest.getTemporalCoverageToInclusive() != null) { + var temporal = Json.createObjectBuilder(); + addNonNull(temporal, Prop.Dcat.START_DATE, uiAssetCreateRequest.getTemporalCoverageFrom()); + addNonNull(temporal, Prop.Dcat.END_DATE, uiAssetCreateRequest.getTemporalCoverageToInclusive()); + properties.add(Prop.Dcterms.TEMPORAL, temporal); + } + + var nutsLocations = uiAssetCreateRequest.getNutsLocations(); + if (uiAssetCreateRequest.getGeoLocation() != null || (nutsLocations != null && !nutsLocations.isEmpty())) { + var spatial = Json.createObjectBuilder(); + addNonNull(spatial, Prop.Skos.PREF_LABEL, uiAssetCreateRequest.getGeoLocation()); + addNonNullArray(spatial, Prop.Dcterms.IDENTIFIER, uiAssetCreateRequest.getNutsLocations()); + properties.add(Prop.Dcterms.SPATIAL, spatial); + } + + if (uiAssetCreateRequest.getDataCategory() != null || uiAssetCreateRequest.getDataSubcategory() != null) { + var mobilityTheme = Json.createObjectBuilder(); + addNonNull(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_CATEGORY, uiAssetCreateRequest.getDataCategory()); + addNonNull(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_SUBCATEGORY, uiAssetCreateRequest.getDataSubcategory()); + properties.add(Prop.MobilityDcatAp.MOBILITY_THEME, mobilityTheme); + } + + var dataAddress = uiAssetCreateRequest.getDataAddressProperties(); + if (dataAddress != null && dataAddress.get(Prop.Edc.TYPE).equals("HttpData")) { + addNonNull(properties, HttpDatasourceHints.BODY, trueIfTrue(dataAddress, Prop.Edc.PROXY_BODY)); + addNonNull(properties, HttpDatasourceHints.PATH, trueIfTrue(dataAddress, Prop.Edc.PROXY_PATH)); + addNonNull(properties, HttpDatasourceHints.QUERY_PARAMS, trueIfTrue(dataAddress, Prop.Edc.PROXY_QUERY_PARAMS)); + addNonNull(properties, HttpDatasourceHints.METHOD, trueIfTrue(dataAddress, Prop.Edc.PROXY_METHOD)); + } + + addNonNull(properties, Prop.SovityDcatExt.CUSTOM_JSON, uiAssetCreateRequest.getCustomJsonAsString()); + val jsonLdStr = uiAssetCreateRequest.getCustomJsonLdAsString(); + if (jsonLdStr != null) { + val jsonLd = JsonUtils.parseJsonObj(jsonLdStr); + for (val e : jsonLd.entrySet()) { + addNonNullJsonValue(properties, e.getKey(), e.getValue()); + } + } + + return properties; + } + + private JsonObjectBuilder getAssetPrivateProperties(UiAssetCreateRequest uiAssetCreateRequest) { + var privateProperties = Json.createObjectBuilder(); + + val privateJsonStr = uiAssetCreateRequest.getPrivateCustomJsonAsString(); + if (privateJsonStr != null) { + addNonNull( + privateProperties, + Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON, + privateJsonStr + ); + } + + val privateJsonLdStr = uiAssetCreateRequest.getPrivateCustomJsonLdAsString(); + if (privateJsonLdStr != null) { + val privateJsonLd = JsonUtils.parseJsonObj(privateJsonLdStr); + privateJsonLd.forEach((k, v) -> addNonNullJsonValue(privateProperties, k, v)); + } + + return privateProperties; + } + + private String trueIfTrue(Map dataAddressProperties, String key) { + return "true".equals(dataAddressProperties.get(key)) ? "true" : "false"; + } + + private JsonObjectBuilder getDataAddress(UiAssetCreateRequest uiAssetCreateRequest) { + var props = edcPropertyUtils.toMapOfObject(uiAssetCreateRequest.getDataAddressProperties()); + return Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder(props)); + } + + private JsonObject removeHandledProperties(JsonObject properties, List handledProperties) { + var remaining = Json.createObjectBuilder(JsonLdUtils.tryCompact(properties)); + handledProperties.forEach(remaining::remove); + return remaining.build(); + } + + private JsonObject getPrivateProperties(JsonObject assetJsonLd) { + if (assetJsonLd.containsKey(Prop.Edc.PRIVATE_PROPERTIES)) { + return JsonLdUtils.object(assetJsonLd, Prop.Edc.PRIVATE_PROPERTIES); + } else if (assetJsonLd.containsKey("privateProperties")) { + // Tests claim this path exists + return JsonLdUtils.object(assetJsonLd, "privateProperties"); + } else { + return JsonValue.EMPTY_JSON_OBJECT; + } + } + + private String buildShortDescription(String description) { + if (description == null) { + return null; + } + + var text = markdownToTextConverter.extractText(description); + return textUtils.abbreviate(text, 300); + } + + private JsonObjectBuilder buildDistribution(UiAssetCreateRequest uiAssetCreateRequest) { + var dataSampleUrls = uiAssetCreateRequest.getDataSampleUrls(); + var referenceFileUrls = uiAssetCreateRequest.getReferenceFileUrls(); + var hasRootLevelFields = uiAssetCreateRequest.getMediaType() != null + || (dataSampleUrls != null && !dataSampleUrls.isEmpty()); + var hasRightsFields = uiAssetCreateRequest.getConditionsForUse() != null; + var hasDataModelFields = uiAssetCreateRequest.getDataModel() != null + && !uiAssetCreateRequest.getDataModel().isBlank(); + var hasReferenceFilesFields = (referenceFileUrls != null && !referenceFileUrls.isEmpty()) + || uiAssetCreateRequest.getReferenceFilesDescription() != null; + + if (!hasRootLevelFields && !hasRightsFields && !hasDataModelFields && !hasReferenceFilesFields) { + return null; + } + + var distribution = Json.createObjectBuilder(); + addNonNull(distribution, Prop.Dcat.MEDIATYPE, uiAssetCreateRequest.getMediaType()); + addNonNullArray(distribution, Prop.Adms.SAMPLE, uiAssetCreateRequest.getDataSampleUrls()); + + if (hasRightsFields) { + var rights = Json.createObjectBuilder(); + addNonNull(rights, Prop.Rdfs.LABEL, uiAssetCreateRequest.getConditionsForUse()); + distribution.add(Prop.Dcterms.RIGHTS, rights); + } + + if (!hasDataModelFields && !hasReferenceFilesFields) { + return distribution; + } + var mobilityDataStandard = Json.createObjectBuilder(); + addNotBlank(mobilityDataStandard, Prop.ID, uiAssetCreateRequest.getDataModel()); + + if (hasReferenceFilesFields) { + var referenceFiles = Json.createObjectBuilder(); + addNonNullArray(referenceFiles, Prop.Dcat.DOWNLOAD_URL, uiAssetCreateRequest.getReferenceFileUrls()); + addNonNull(referenceFiles, Prop.Rdfs.LITERAL, uiAssetCreateRequest.getReferenceFilesDescription()); + mobilityDataStandard.add(Prop.MobilityDcatAp.SCHEMA, referenceFiles); + } + distribution.add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, mobilityDataStandard); + return distribution; + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java new file mode 100644 index 000000000..ff5a5b515 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java @@ -0,0 +1,263 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.SneakyThrows; +import net.javacrumbs.jsonunit.assertj.JsonAssertions; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static jakarta.json.Json.createArrayBuilder; +import static jakarta.json.Json.createObjectBuilder; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class AssetMapperTest { + AssetMapper assetMapper; + + String endpoint = "https://my-connector/api/dsp"; + String participantId = "my-connector"; + + @BeforeEach + void setup() { + var jsonLd = new TitaniumJsonLd(mock(Monitor.class)); + var typeTransformerRegistry = mock(TypeTransformerRegistry.class); + var uiAssetBuilder = new UiAssetMapper(new EdcPropertyUtils(), new AssetJsonLdUtils(), new MarkdownToTextConverter(), new TextUtils(), x -> endpoint.equals(x)); + assetMapper = new AssetMapper(typeTransformerRegistry, uiAssetBuilder, jsonLd); + } + + @Test + @SneakyThrows + void test_buildAssetDto() { + // Arrange + String assetJsonLd = TestUtils.loadResourceAsString("/example-asset.jsonld"); + + // Act + var uiAsset = assetMapper.buildUiAsset(JsonUtils.parseJsonObj(assetJsonLd), endpoint, participantId); + + // Assert + assertThat(uiAsset.getAssetId()).isEqualTo("urn:artifact:my-asset"); + assertThat(uiAsset.getConnectorEndpoint()).isEqualTo(endpoint); + assertThat(uiAsset.getParticipantId()).isEqualTo(participantId); + assertThat(uiAsset.getTitle()).isEqualTo("My Asset"); + assertThat(uiAsset.getLanguage()).isEqualTo("https://w3id.org/idsa/code/EN"); + assertThat(uiAsset.getDescription()).isEqualTo( + "# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"); + assertThat(uiAsset.getDescriptionShortText()).isEqualTo( + "Lorem Ipsum... h2 title Link text Here 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + assertThat(uiAsset.getIsOwnConnector()).isEqualTo(true); + assertThat(uiAsset.getCreatorOrganizationName()).isEqualTo("My Organization Name"); + assertThat(uiAsset.getPublisherHomepage()).isEqualTo("https://data-source.my-org/about"); + assertThat(uiAsset.getLicenseUrl()).isEqualTo("https://data-source.my-org/license"); + assertThat(uiAsset.getVersion()).isEqualTo("1.1"); + assertThat(uiAsset.getKeywords()).isEqualTo(List.of("some", "keywords")); + assertThat(uiAsset.getMediaType()).isEqualTo("application/json"); + assertThat(uiAsset.getLandingPageUrl()).isEqualTo("https://data-source.my-org/docs"); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isTrue(); + assertThat(uiAsset.getHttpDatasourceHintsProxyPath()).isTrue(); + assertThat(uiAsset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); + assertThat(uiAsset.getHttpDatasourceHintsProxyBody()).isTrue(); + assertThat(uiAsset.getDataCategory()).isEqualTo("Infrastructure and Logistics"); + assertThat(uiAsset.getDataSubcategory()).isEqualTo("General Information About Planning Of Routes"); + assertThat(uiAsset.getDataModel()).isEqualTo("my-data-model-001"); + assertThat(uiAsset.getGeoReferenceMethod()).isEqualTo("my-geo-reference-method"); + assertThat(uiAsset.getTransportMode()).isEqualTo("my-transport-mode"); + assertThat(uiAsset.getSovereignLegalName()).isEqualTo("my-sovereign"); + assertThat(uiAsset.getGeoLocation()).isEqualTo("my-geolocation"); + assertThat(uiAsset.getNutsLocations()).isEqualTo(Arrays.asList("my-nuts-location1", "my-nuts-location2")); + assertThat(uiAsset.getDataSampleUrls()).isEqualTo(Arrays.asList("my-data-sample-urls1", "my-data-sample-urls2")); + assertThat(uiAsset.getReferenceFileUrls()).isEqualTo(Arrays.asList("my-reference-files1", "my-reference-files2")); + assertThat(uiAsset.getReferenceFilesDescription()).isEqualTo("my-additional-description"); + assertThat(uiAsset.getConditionsForUse()).isEqualTo("my-conditions-for-use"); + assertThat(uiAsset.getDataUpdateFrequency()).isEqualTo("my-data-update-frequency"); + assertThat(uiAsset.getTemporalCoverageFrom()).isEqualTo("2007-12-03"); + assertThat(uiAsset.getTemporalCoverageToInclusive()).isEqualTo("2024-01-22"); + + assertThat(uiAsset.getAssetJsonLd()).contains("\"%s\"".formatted(Prop.Edc.ID)); + + JsonAssertions.assertThatJson(uiAsset.getCustomJsonAsString()) + .isEqualTo(""" + { + "array": [3, 1, 4, 1, 5], + "boolean": false, + "null": null, + "number": 116, + "object": { + "key": "value" + }, + "string": "value" + } + """); + JsonAssertions.assertThatJson(uiAsset.getCustomJsonLdAsString()) + .isObject() + .containsEntry("http://unknown/some-custom-string", "some-string-value") + .containsEntry("http://unknown/some-custom-obj", json(""" + { "http://unknown/a": "b" } + """)); + + JsonAssertions.assertThatJson(uiAsset.getPrivateCustomJsonAsString()) + .isEqualTo(""" + { + "priv_array": [3, 1, 4, 1, 5], + "priv_boolean": false, + "priv_null": null, + "priv_number": 116, + "priv_object": { + "key": "value" + }, + "priv_string": "value" + } + """); + JsonAssertions.assertThatJson(uiAsset.getPrivateCustomJsonLdAsString()) + .isObject() + .containsEntry("http://unknown/some-custom-private-string", "some-private-value") + .containsEntry("http://unknown/some-custom-private-obj", json(""" + { + "http://unknown/a-private": "b-private" + } + """)); + } + + @Test + void test_empty() { + + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .build(); + + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getAssetId()).isEqualTo("my-asset-1"); + assertThat(uiAsset.getTitle()).isEqualTo("my-asset-1"); + } + + @Test + void test_KeywordsAsSingleString() { + + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.Dcat.KEYWORDS, "SingleElement") + .build()) + .build(); + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getKeywords()).isEqualTo(List.of("SingleElement")); + } + + @Test + void test_StringValueWrappedInAtValue() { + + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.Dcterms.TITLE, createObjectBuilder() + .add(Prop.VALUE, "AssetTitle") + .add(Prop.LANGUAGE, "en"))) + .build(); + + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getTitle()).isEqualTo("AssetTitle"); + } + + @Test + void test_StringsAsMap() { + + // Arrange + var properties = createObjectBuilder() + .add(Prop.Dcterms.TITLE, createArrayBuilder() + .add(createObjectBuilder() + .add(Prop.TYPE, "SomeType") + .add(Prop.VALUE, "AssetTitle") + ) + ) + .build(); + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, properties) + .build(); + + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getTitle()).isEqualTo("AssetTitle"); + } + + @Test + void test_badBooleanValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "wrongBooleanValue") + .build()) + .build(); + + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); + } + + @Test + void test_noBooleanValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "") + .build()) + .build(); + + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); + } + + @Test + void test_isNotOwnConnector() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .build(); + + // Act + var uiAsset = assetMapper.buildUiAsset(assetJsonLd, "https://other-connector/api/dsp", participantId); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getIsOwnConnector()).isFalse(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java new file mode 100644 index 000000000..70af31f2f --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java @@ -0,0 +1,52 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; +import org.assertj.core.api.Assertions; +import org.eclipse.edc.policy.model.Operator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class OperatorMapperTest { + OperatorMapper operatorMapper; + + @BeforeEach + void setup() { + operatorMapper = new OperatorMapper(); + } + + @Test + void test_getOperator() { + Arrays.stream(OperatorDto.values()).forEach(dto -> { + Operator operator = operatorMapper.getOperator(dto); + assertThat(operator.name()).isEqualTo(dto.name()); + }); + } + + @Test + void test_getOperatorDto_OperatorDto() { + Arrays.stream(Operator.values()).forEach(op -> { + var dto = operatorMapper.getOperatorDto(op); + assertThat(op.name()).isEqualTo(dto.name()); + }); + } + + @Test + void test_getOperatorDto_String() { + Arrays.stream(Operator.values()).forEach(op -> { + var dto = operatorMapper.getOperatorDto(op.name()); + assertThat(op.name()).isEqualTo(dto.name()); + }); + } + + @Test + void test_getOperatorDto_String_caseInsensitivity() { + String operatorString = "eQ"; + OperatorDto operatorDto = operatorMapper.getOperatorDto(operatorString); + Assertions.assertThat(operatorDto).isEqualTo(OperatorDto.EQ); + } +} + diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java new file mode 100644 index 000000000..13fe9ee76 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java @@ -0,0 +1,91 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import jakarta.json.JsonObject; +import lombok.SneakyThrows; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static de.sovity.edc.utils.JsonUtils.parseJsonObj; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PolicyMapperTest { + @InjectMocks + PolicyMapper policyMapper; + + @Mock + TypeTransformerRegistry transformerRegistry; + + @Mock + ConstraintExtractor constraintExtractor; + + @Mock + AtomicConstraintMapper atomicConstraintMapper; + + + @Test + @SneakyThrows + void test_buildPolicyDto() { + try (MockedStatic mappingErrors = mockStatic(MappingErrors.class)) { + // arrange + var policy = mock(Policy.class); + var errors = mock(MappingErrors.class); + var constraints = List.of(mock(UiPolicyConstraint.class)); + + when(errors.getErrors()).thenReturn(List.of("error1")); + + mappingErrors.when(MappingErrors::root).thenReturn(errors); + when(constraintExtractor.getPermissionConstraints(policy, errors)).thenReturn(constraints); + when(transformerRegistry.transform(policy, JsonObject.class)).thenReturn(Result.success(parseJsonObj("{}"))); + + // act + var actual = policyMapper.buildUiPolicy(policy); + + // assert + assertThat(actual.getPolicyJsonLd()).isEqualTo("{}"); + assertThat(actual.getConstraints()).isEqualTo(constraints); + assertThat(actual.getErrors()).isEqualTo(List.of("error1")); + } + } + + @Test + void test_buildPolicy() { + // arrange + var constraint = mock(UiPolicyConstraint.class); + var createRequest = new UiPolicyCreateRequest(List.of(constraint)); + + var expected = mock(AtomicConstraint.class); + when(atomicConstraintMapper.buildAtomicConstraints(eq(List.of(constraint)))) + .thenReturn(List.of(expected)); + + // act + var actual = policyMapper.buildPolicy(createRequest); + + // assert + assertThat(actual.getType()).isEqualTo(PolicyType.SET); + assertThat(actual.getPermissions()).hasSize(1); + assertThat(actual.getPermissions().get(0).getConstraints()).hasSize(1); + assertThat(actual.getPermissions().get(0).getAction().getType()).isEqualTo("USE"); + assertThat(actual.getPermissions().get(0).getConstraints().get(0)).isSameAs(expected); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java new file mode 100644 index 000000000..113f847f1 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java @@ -0,0 +1,15 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Files; +import java.nio.file.Paths; + +public class TestUtils { + @NotNull + @SneakyThrows + public static String loadResourceAsString(String name) { + return new String(Files.readAllBytes(Paths.get(TestUtils.class.getResource(name).toURI()))); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java new file mode 100644 index 000000000..70fed07d0 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java @@ -0,0 +1,240 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteralType; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Expression; +import org.eclipse.edc.policy.model.LiteralExpression; +import org.eclipse.edc.policy.model.Operator; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +class AtomicConstraintMapperTest { + + @Test + void test_buildAtomicConstraint_null() { + // arrange + var atomicConstraintMapper = newAtomicConstraintMapper(); + + // act + var actual = atomicConstraintMapper.buildAtomicConstraints(null); + + // assert + assertThat(actual).isEmpty(); + } + + @Test + void test_buildAtomicConstraint() { + // arrange + var literalMapper = mock(LiteralMapper.class); + var atomicConstraintMapper = newAtomicConstraintMapper(literalMapper); + + var right = mock(UiPolicyLiteral.class); + var constraint = new UiPolicyConstraint("left", OperatorDto.EQ, right); + + when(literalMapper.getUiLiteralValue(right)).thenReturn("right"); + + // act + var actual = atomicConstraintMapper.buildAtomicConstraints(List.of(constraint)); + + // assert + assertThat(actual).hasSize(1); + var atomicConstraint = actual.get(0); + assertThat(atomicConstraint.getLeftExpression()).isInstanceOfSatisfying(LiteralExpression.class, literalExpression -> + assertThat(literalExpression.getValue()).isEqualTo("left")); + assertThat(atomicConstraint.getRightExpression()).isInstanceOfSatisfying(LiteralExpression.class, literalExpression -> + assertThat(literalExpression.getValue()).isEqualTo("right")); + assertThat(atomicConstraint.getOperator()).isEqualTo(Operator.EQ); + } + + @Test + void test_buildUiConstraint_string() { + // arrange + var atomicConstraintMapper = newAtomicConstraintMapper(); + var errors = MappingErrors.root(); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression("left")) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression("right")) + .build(); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(errors.getErrors()).isEmpty(); + assertThat(actual).isPresent(); + assertThat(actual.get().getLeft()).isEqualTo("left"); + assertThat(actual.get().getOperator()).isEqualTo(OperatorDto.EQ); + assertThat(actual.get().getRight().getType()).isEqualTo(UiPolicyLiteralType.STRING); + assertThat(actual.get().getRight().getValue()).isEqualTo("right"); + } + + @Test + void test_buildUiConstraint_stringList() { + // arrange + var atomicConstraintMapper = newAtomicConstraintMapper(); + var errors = MappingErrors.root(); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression("left")) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression(List.of("right"))) + .build(); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(errors.getErrors()).isEmpty(); + assertThat(actual).isPresent(); + assertThat(actual.get().getLeft()).isEqualTo("left"); + assertThat(actual.get().getOperator()).isEqualTo(OperatorDto.EQ); + assertThat(actual.get().getRight().getType()).isEqualTo(UiPolicyLiteralType.STRING_LIST); + assertThat(actual.get().getRight().getValueList()).isEqualTo(List.of("right")); + } + + @Test + void test_buildUiConstraint_json() { + // arrange + var atomicConstraintMapper = newAtomicConstraintMapper(); + var errors = MappingErrors.root(); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression("left")) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression(Map.of("a", "b"))) + .build(); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(errors.getErrors()).isEmpty(); + assertThat(actual).isPresent(); + assertThat(actual.get().getLeft()).isEqualTo("left"); + assertThat(actual.get().getOperator()).isEqualTo(OperatorDto.EQ); + assertThat(actual.get().getRight().getType()).isEqualTo(UiPolicyLiteralType.JSON); + assertThat(actual.get().getRight().getValue()).isEqualTo("{\"a\":\"b\"}"); + } + + @Test + void test_buildUiConstraint_string2() { + // arrange + var atomicConstraintMapper = newAtomicConstraintMapper(); + var errors = MappingErrors.root(); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression("left")) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression("right")) + .build(); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(errors.getErrors()).isEmpty(); + assertThat(actual).isPresent(); + assertThat(actual.get().getLeft()).isEqualTo("left"); + assertThat(actual.get().getOperator()).isEqualTo(OperatorDto.EQ); + assertThat(actual.get().getRight().getType()).isEqualTo(UiPolicyLiteralType.STRING); + assertThat(actual.get().getRight().getValue()).isEqualTo("right"); + } + + @Test + void test_buildUiConstraint_leftBad() { + // arrange + var literalMapper = mock(LiteralMapper.class); + var atomicConstraintMapper = newAtomicConstraintMapper(literalMapper); + var errors = MappingErrors.root(); + + var leftExpression = mock(Expression.class); + var rightExpression = mock(Expression.class); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(leftExpression) + .operator(Operator.EQ) + .rightExpression(rightExpression) + .build(); + + when(literalMapper.getExpressionString(same(leftExpression), any())).thenAnswer(i -> { + i.getArgument(1, MappingErrors.class).add("my-error"); + return Optional.empty(); + }); + when(literalMapper.getExpressionValue(same(rightExpression), any())).thenReturn(Optional.of(mock(UiPolicyLiteral.class))); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$.leftExpression: my-error"); + } + + @Test + void test_buildUiConstraint_rightBad() { + // arrange + var literalMapper = mock(LiteralMapper.class); + var atomicConstraintMapper = newAtomicConstraintMapper(literalMapper); + var errors = MappingErrors.root(); + + var leftExpression = mock(Expression.class); + var rightExpression = mock(Expression.class); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(leftExpression) + .operator(Operator.EQ) + .rightExpression(rightExpression) + .build(); + + when(literalMapper.getExpressionString(same(leftExpression), any())).thenReturn(Optional.of("left")); + when(literalMapper.getExpressionValue(same(rightExpression), any())).thenAnswer(i -> { + i.getArgument(1, MappingErrors.class).add("my-error"); + return Optional.empty(); + }); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$.rightExpression: my-error"); + } + + @Test + void test_buildUiConstraint_operatorBad() { + // arrange + var atomicConstraintMapper = newAtomicConstraintMapper(); + var errors = MappingErrors.root(); + var atomicConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression("left")) + .operator(null) + .rightExpression(new LiteralExpression("right")) + .build(); + + // act + var actual = atomicConstraintMapper.buildUiConstraint(atomicConstraint, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$.operator: Operator is null."); + } + + private AtomicConstraintMapper newAtomicConstraintMapper() { + return newAtomicConstraintMapper(new LiteralMapper(new ObjectMapper())); + } + + private AtomicConstraintMapper newAtomicConstraintMapper(LiteralMapper literalMapper) { + return new AtomicConstraintMapper(literalMapper, new OperatorMapper()); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java new file mode 100644 index 000000000..9b9d52c55 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java @@ -0,0 +1,89 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import org.eclipse.edc.policy.model.AndConstraint; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.OrConstraint; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.XoneConstraint; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ConstraintExtractorTest { + @InjectMocks + ConstraintExtractor constraintExtractor; + + @Mock + PolicyValidator policyValidator; + + @Mock + AtomicConstraintMapper atomicConstraintMapper; + + @Test + void test_getPermissionConstraints_null() { + // arrange + var policy = Policy.Builder.newInstance().build(); + var errors = MappingErrors.root(); + + // act + var actual = constraintExtractor.getPermissionConstraints(policy, errors); + + // assert + assertThat(actual).isEmpty(); + verify(policyValidator).validateOtherPolicyFieldsUnset(policy, errors); + } + + @Test + void test_getPermissionConstraints_many_constraints() { + // arrange + var first = mock(AtomicConstraint.class); + var other = mock(AtomicConstraint.class); + var permission = Permission.Builder.newInstance() + .constraint(null) + .constraint(first) + .constraint(other) + .constraint(mock(AndConstraint.class)) + .constraint(mock(OrConstraint.class)) + .constraint(mock(XoneConstraint.class)) + .build(); + var policy = Policy.Builder.newInstance() + .permission(null) + .permission(permission) + .permission(Permission.Builder.newInstance().build()) + .build(); + var errors = MappingErrors.root(); + + var expected = mock(UiPolicyConstraint.class); + when(atomicConstraintMapper.buildUiConstraint(same(first), any())).thenReturn(Optional.of(expected)); + when(atomicConstraintMapper.buildUiConstraint(same(other), any())).thenReturn(Optional.empty()); + + // act + var actual = constraintExtractor.getPermissionConstraints(policy, errors); + + // assert + verify(policyValidator).validateOtherPermissionFieldsUnset(same(permission), any()); + verify(policyValidator).validateOtherPermissionFieldsUnset(eq(null), any()); + assertThat(actual).containsExactly(expected); + assertThat(errors.getErrors()).containsExactlyInAnyOrder( + "$.permissions[1].constraints[0]: Constraint is null.", + "$.permissions[1].constraints[3]: AndConstraints are currently unsupported.", + "$.permissions[1].constraints[4]: OrConstraints are currently unsupported.", + "$.permissions[1].constraints[5]: XoneConstraints are currently unsupported." + ); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java new file mode 100644 index 000000000..d95246d45 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class EdcPropertyUtilsTest { + EdcPropertyUtils edcPropertyUtils; + + @BeforeEach + void setup() { + edcPropertyUtils = new EdcPropertyUtils(); + } + + @Test + void testToObjectMap() { + assertThat(edcPropertyUtils.toMapOfObject(Map.of( + "a", "b" + ))).isEqualTo(Map.of( + "a", "b" + )); + } + + @Test + void testToStringMap() { + // arrange + Map map = new HashMap<>(); + map.put("a", "b"); + map.put("c", 1); + map.put("d", 2.0); + map.put("e", null); + map.put("f", new HashMap<>()); + + // act + var actual = edcPropertyUtils.truncateToMapOfString(map); + + // assert + Map expected = new HashMap<>(); + expected.put("a", "b"); + expected.put("c", "1"); + expected.put("d", "2.0"); + expected.put("e", null); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java new file mode 100644 index 000000000..2f292c888 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java @@ -0,0 +1,254 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteralType; +import org.eclipse.edc.policy.model.Expression; +import org.eclipse.edc.policy.model.LiteralExpression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LiteralMapperTest { + LiteralMapper literalMapper; + + @BeforeEach + void setup() { + literalMapper = new LiteralMapper(new ObjectMapper()); + } + + @Test + void test_getUiLiteralValue_string() { + // arrange + var literal = UiPolicyLiteral.ofString("test"); + + // act + var actual = literalMapper.getUiLiteralValue(literal); + + // assert + assertThat(actual).isEqualTo("test"); + } + + @Test + void test_getUiLiteralValue_stringNull() { + // arrange + var literal = UiPolicyLiteral.ofString(null); + + // act + var actual = literalMapper.getUiLiteralValue(literal); + + // assert + assertThat(actual).isNull(); + } + + @Test + void test_getUiLiteralValue_list() { + // arrange + var literal = UiPolicyLiteral.ofStringList(List.of("test")); + + // act + var actual = literalMapper.getUiLiteralValue(literal); + + // assert + assertThat(actual).isEqualTo(List.of("test")); + } + + @Test + void test_getUiLiteralValue_json() { + // arrange + var literal = UiPolicyLiteral.ofJson("true"); + + // act + var actual = literalMapper.getUiLiteralValue(literal); + + // assert + assertThat(actual).isEqualTo(true); + } + + @Test + void test_getLiteralExpression_null() { + // arrange + var expression = (Expression) null; + var errors = MappingErrors.root(); + + // act + var actual = literalMapper.getLiteralExpression(expression, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$: Expression is null."); + } + + @Test + void test_getLiteralExpression_otherSubclass() { + // arrange + var expression = mock(Expression.class); + var errors = MappingErrors.root(); + + // act + var actual = literalMapper.getLiteralExpression(expression, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).hasSize(1); + assertThat(errors.getErrors().get(0)).startsWith("$: Expression type is not LiteralExpression, but "); + } + + @Test + void test_getLiteralExpression_ok() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + // act + var actual = literalMapper.getLiteralExpression(expression, errors); + + // assert + assertThat(actual).contains(expression); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getExpressionString_null() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn(null); + + // act + var actual = literalMapper.getExpressionString(expression, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$.value: Is not a string, but null."); + } + + @Test + void test_getExpressionString_ok() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn("test"); + + // act + var actual = literalMapper.getExpressionString(expression, errors); + + // assert + assertThat(actual).contains("test"); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getExpressionString_notAString() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn(5); + + // act + var actual = literalMapper.getExpressionString(expression, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$.value: Is not a string."); + } + + @Test + void test_getExpressionValue_null() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn(null); + + // act + var actual = literalMapper.getExpressionValue(expression, errors); + + // assert + assertThat(actual).isNotEmpty(); + assertThat(actual.get().getType()).isEqualTo(UiPolicyLiteralType.JSON); + assertThat(actual.get().getValue()).isEqualTo("null"); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getExpressionValue_string() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn("test"); + + // act + var actual = literalMapper.getExpressionValue(expression, errors); + + // assert + assertThat(actual).isNotEmpty(); + assertThat(actual.get().getType()).isEqualTo(UiPolicyLiteralType.STRING); + assertThat(actual.get().getValue()).isEqualTo("test"); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getExpressionValue_string_list() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn(Arrays.asList(null, "test")); + + // act + var actual = literalMapper.getExpressionValue(expression, errors); + + // assert + assertThat(actual).isNotEmpty(); + assertThat(actual.get().getType()).isEqualTo(UiPolicyLiteralType.STRING_LIST); + assertThat(actual.get().getValueList()).isEqualTo(Arrays.asList(null, "test")); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getExpressionValue_string_json() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn(true); + + // act + var actual = literalMapper.getExpressionValue(expression, errors); + + // assert + assertThat(actual).isNotEmpty(); + assertThat(actual.get().getType()).isEqualTo(UiPolicyLiteralType.JSON); + assertThat(actual.get().getValue()).isEqualTo("true"); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getExpressionValue_other_list() { + // arrange + var expression = mock(LiteralExpression.class); + var errors = MappingErrors.root(); + + when(expression.getValue()).thenReturn(Arrays.asList("string", 5)); + + // act + var actual = literalMapper.getExpressionValue(expression, errors); + + // assert + assertThat(actual).isNotEmpty(); + assertThat(actual.get().getType()).isEqualTo(UiPolicyLiteralType.JSON); + assertThat(actual.get().getValue()).isEqualTo("[\"string\",5]"); + assertThat(errors.getErrors()).isEmpty(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java new file mode 100644 index 000000000..6512cbf15 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java @@ -0,0 +1,27 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MappingErrorsTest { + + + @Test + void testMappingErrors() { + var parent = MappingErrors.root(); + parent.add("a"); + + var childObj = parent.forChildObject("child"); + childObj.add("b"); + + var childObjArray = childObj.forChildArrayElement(3); + childObjArray.add("c"); + + assertThat(parent.getErrors()).containsExactly( + "$: a", + "$.child: b", + "$.child[3]: c" + ); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java new file mode 100644 index 000000000..e18a79bd4 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java @@ -0,0 +1,158 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; +import org.eclipse.edc.policy.model.Prohibition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class PolicyValidatorTest { + @InjectMocks + PolicyValidator policyValidator; + + @Test + void testPolicy_null() { + // arrange + var errors = MappingErrors.root(); + var policy = (Policy) null; + + // act + policyValidator.validateOtherPolicyFieldsUnset(policy, errors); + + // assert + assertThat(errors.getErrors()).containsExactly("$: Policy is null"); + } + + @Test + void testPolicy_ok() { + // arrange + var errors = MappingErrors.root(); + var policy = Policy.Builder.newInstance() + .type(PolicyType.SET) + .permission(mock(Permission.class)) + .build(); + + // act + policyValidator.validateOtherPolicyFieldsUnset(policy, errors); + + // assert + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void testPolicy_full() { + // arrange + var errors = MappingErrors.root(); + var policy = Policy.Builder.newInstance() + .prohibition(mock(Prohibition.class)) + .duty(mock(Duty.class)) + .inheritsFrom("inheritsFrom") + .assigner("assigner") + .assignee("assignee") + .target("target") + .type(PolicyType.OFFER) + .extensibleProperty("some", "prop") + .build(); + + // act + policyValidator.validateOtherPolicyFieldsUnset(policy, errors); + + // assert + assertThat(errors.getErrors()).containsExactlyInAnyOrder( + "$: Policy has no permissions.", + "$: Policy has prohibitions, which are currently unsupported.", + "$: Policy has obligations, which are currently unsupported.", + "$: Policy has inheritsFrom, which is currently unsupported.", + "$: Policy has an assigner, which is currently unsupported.", + "$: Policy has an assignee, which is currently unsupported.", + "$: Policy has extensible properties.", + "$: Policy does not have type SET, but OFFER, which is currently unsupported." + ); + } + + @Test + void testPermission_null() { + // arrange + var errors = MappingErrors.root(); + var permission = (Permission) null; + + // act + policyValidator.validateOtherPermissionFieldsUnset(permission, errors); + + // assert + assertThat(errors.getErrors()).containsExactly("$: Permission is null."); + } + + @Test + void testPermission_action_null() { + // arrange + var errors = MappingErrors.root(); + var permission = Permission.Builder.newInstance() + .constraint(mock(Constraint.class)) + .build(); + + // act + policyValidator.validateOtherPermissionFieldsUnset(permission, errors); + + // assert + assertThat(errors.getErrors()).containsExactly("$.action: Action is null."); + } + + @Test + void testPermission_ok() { + // arrange + var errors = MappingErrors.root(); + var permission = Permission.Builder.newInstance() + .constraint(mock(Constraint.class)) + .action(Action.Builder.newInstance().type("USE").build()) + .build(); + + // act + policyValidator.validateOtherPermissionFieldsUnset(permission, errors); + + // assert + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void testPermission_full() { + // arrange + var errors = MappingErrors.root(); + var action = Action.Builder.newInstance() + .type("idk") + .constraint(mock(Constraint.class)) + .includedIn("includedIn") + .build(); + var permission = Permission.Builder.newInstance() + .duty(mock(Duty.class)) + .assigner("assigner") + .assignee("assignee") + .target("target") + .action(action) + .build(); + + // act + policyValidator.validateOtherPermissionFieldsUnset(permission, errors); + + // assert + assertThat(errors.getErrors()).containsExactlyInAnyOrder( + "$: Permission has no constraints.", + "$: Permission has duties, which is currently unsupported.", + "$: Permission has an assigner, which is currently unsupported.", + "$: Permission has an assignee, which is currently unsupported.", + "$.action: Action has a type that is not 'USE', but 'idk'.", + "$.action: Action has a value for includedIn, which is currently unsupported.", + "$.action: Action has a constraint, which is currently unsupported." + ); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java new file mode 100644 index 000000000..92ffe3c80 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java @@ -0,0 +1,63 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TextUtilsTest { + TextUtils textUtils; + + @BeforeEach + void setup() { + textUtils = new TextUtils(); + } + + @Test + void test_abbreviate_null() { + // arrange + String text = null; + + // act + var actual = textUtils.abbreviate(text, 1); + + // assert + assertThat(actual).isEqualTo(null); + } + + @Test + void test_abbreviate_emptyString() { + // arrange + var text = ""; + + // act + var actual = textUtils.abbreviate(text, 1); + + // assert + assertThat(actual).isEqualTo(""); + } + + @Test + void test_abbreviate_lengthLessThanMaxCharacters() { + // arrange + var text = "a"; + + // act + var actual = textUtils.abbreviate(text, 2); + + // assert + assertThat(actual).isEqualTo("a"); + } + + @Test + void test_abbreviate_lengthLongerThanMaxCharacters() { + // arrange + var text = "aa"; + + // act + var actual = textUtils.abbreviate(text, 1); + + // assert + assertThat(actual).isEqualTo("a"); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java new file mode 100644 index 000000000..27b21a0d9 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java @@ -0,0 +1,306 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers.utils; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class UiAssetMapperTest { + UiAssetMapper uiAssetMapper; + EdcPropertyUtils edcPropertyUtils; + + private static final String ASSET_ID = "asset-id"; + private static final String ORG_NAME = "org-name"; + + @BeforeEach + void setup() { + var assetJsonLdUtils = mock(AssetJsonLdUtils.class); + var markdownToTextConverter = mock(MarkdownToTextConverter.class); + var textUtils = mock(TextUtils.class); + var ownConnectorEndpointService = mock(OwnConnectorEndpointService.class); + edcPropertyUtils = new EdcPropertyUtils(); + + uiAssetMapper = new UiAssetMapper(edcPropertyUtils, assetJsonLdUtils, markdownToTextConverter, textUtils, ownConnectorEndpointService); + } + + @Test + void test_buildAssetJsonLd_only_id() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); + assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); + } + + @Test + void test_buildAssetJsonLd_empty_nuts() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setNutsLocations(List.of()); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); + } + + @Test + void test_buildAssetJsonLd_empty_reference_file_urls() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setReferenceFileUrls(List.of()); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); + } + + @Test + void test_buildAssetJsonLd_empty_data_sample_urls() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataSampleUrls(List.of()); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); + } + + // The following functions test paths of buildDistribution + @Test + void test_buildAssetJsonLd_distribution1() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setMediaType("B"); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); + var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); + assertThat(properties).hasSize(3); + var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); + assertThat(distribution).hasSize(1); + assertThat(distribution.getString(Prop.Dcat.MEDIATYPE)).isEqualTo("B"); + } + + @Test + void test_buildAssetJsonLd_distribution2() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setConditionsForUse("B"); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); + var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); + assertThat(properties).hasSize(3); + var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); + assertThat(distribution).hasSize(1); + var rights = distribution.getJsonObject(Prop.Dcterms.RIGHTS); + assertThat(rights).hasSize(1); + assertThat(rights.getString(Prop.Rdfs.LABEL)).isEqualTo("B"); + } + + @Test + void test_buildAssetJsonLd_distribution3() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataModel("B"); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); + var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); + assertThat(properties).hasSize(3); + var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); + assertThat(distribution).hasSize(1); + var mobilityDataStandard = distribution.getJsonObject(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); + assertThat(mobilityDataStandard).hasSize(1); + assertThat(mobilityDataStandard.getString(Prop.ID)).isEqualTo("B"); + } + + @Test + void test_buildAssetJsonLd_distribution4() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setReferenceFilesDescription("B"); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThat(actual).isNotNull(); + assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); + var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); + assertThat(properties).hasSize(3); + var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); + assertThat(distribution).hasSize(1); + var mobilityDataStandard = distribution.getJsonObject(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); + assertThat(mobilityDataStandard).hasSize(1); + var referenceFiles = mobilityDataStandard.getJsonObject(Prop.MobilityDcatAp.SCHEMA); + assertThat(referenceFiles).hasSize(1); + assertThat(referenceFiles.getString(Prop.Rdfs.LITERAL)).isEqualTo("B"); + } + + @Test + void test_buildAssetJsonLd_data_model_nonNull() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataModel("B"); + + var expected = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.ID, "B"))); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThatJson(JsonUtils.toJson(actual)) + .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); + } + + @Test + void test_buildAssetJsonLd_data_model_null() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataModel(null); + + var expected = Json.createObjectBuilder(); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThatJson(JsonUtils.toJson(actual)) + .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); + } + + @Test + void test_buildAssetJsonLd_data_model_blank() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataModel(" "); + + var expected = Json.createObjectBuilder(); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThatJson(JsonUtils.toJson(actual)) + .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); + } + + @Test + void test_buildAssetJsonLd_data_model_blank_but_also_reference_files_desc() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataModel(" "); + uiAssetCreateRequest.setReferenceFilesDescription("test"); + + var expected = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() + .add(Prop.Rdfs.LITERAL, "test")))); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThatJson(JsonUtils.toJson(actual)) + .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); + } + + @Test + void test_buildAssetJsonLd_data_model_blank_but_also_reference_file_urls() { + // arrange + var uiAssetCreateRequest = new UiAssetCreateRequest(); + uiAssetCreateRequest.setId(ASSET_ID); + uiAssetCreateRequest.setDataModel(" "); + uiAssetCreateRequest.setReferenceFileUrls(List.of("http://test")); + + var expected = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() + .add(Prop.Dcat.DOWNLOAD_URL, Json.createArrayBuilder().add("http://test"))))); + + // act + var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertThatJson(JsonUtils.toJson(actual)) + .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); + } + + /** + * Creates Asset JSON LD from additional properties + *

      + * Let the above tests be more readable + * + * @param properties additional Asset JSON-LD Properties + * @return Asset JSON LD + */ + private JsonObject buildAssetJsonLd(JsonObjectBuilder properties) { + return Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_ASSET) + .add(Prop.ID, ASSET_ID) + .add(Prop.Edc.DATA_ADDRESS, Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder())) + .add(Prop.Edc.PROPERTIES, properties + .add(Prop.Edc.ID, ASSET_ID) + .add(Prop.Dcterms.CREATOR, Json.createObjectBuilder() + .add(Prop.Foaf.NAME, ORG_NAME)) + ) + .add(Prop.Edc.PRIVATE_PROPERTIES, Json.createObjectBuilder()) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld new file mode 100644 index 000000000..1334b829c --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld @@ -0,0 +1,82 @@ +{ + "@id": "urn:artifact:my-asset", + "@type": "https://w3id.org/edc/v0.0.1/ns/Asset", + "https://w3id.org/edc/v0.0.1/ns/properties": { + "https://w3id.org/edc/v0.0.1/ns/id": "urn:artifact:my-asset", + "http://purl.org/dc/terms/identifier": "urn:artifact:my-asset", + "http://purl.org/dc/terms/title": "My Asset", + "http://purl.org/dc/terms/language": "https://w3id.org/idsa/code/EN", + "http://purl.org/dc/terms/description": "# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "http://purl.org/dc/terms/creator": { + "@type": "http://xmlns.com/foaf/0.1/Organization", + "http://xmlns.com/foaf/0.1/name": "My Organization Name" + }, + "http://www.w3.org/ns/dcat#distribution": { + "@type": "http://www.w3.org/ns/dcat#Distribution", + "http://www.w3.org/ns/dcat#mediaType": "application/json", + "https://w3id.org/mobilitydcat-ap/mobilityDataStandard": { + "@id": "my-data-model-001", + "https://w3id.org/mobilitydcat-ap/schema": { + "http://www.w3.org/ns/dcat#downloadURL": [ + "my-reference-files1", + "my-reference-files2" + ], + "http://www.w3.org/2000/01/rdf-schema#Literal": "my-additional-description" + } + }, + "http://www.w3.org/ns/adms#sample": [ + "my-data-sample-urls1", + "my-data-sample-urls2" + ], + "http://purl.org/dc/terms/rights": { + "http://www.w3.org/2000/01/rdf-schema#label": "my-conditions-for-use" + } + }, + "http://purl.org/dc/terms/publisher": { + "@type": "http://xmlns.com/foaf/0.1/Organization", + "http://xmlns.com/foaf/0.1/homepage": "https://data-source.my-org/about" + }, + "http://purl.org/dc/terms/license": "https://data-source.my-org/license", + "http://www.w3.org/ns/dcat#version": "1.1", + "http://www.w3.org/ns/dcat#keyword": [ + "some", + "keywords" + ], + "http://www.w3.org/ns/dcat#landingPage": "https://data-source.my-org/docs", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod": "true", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath": "true", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams": "true", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody": "true", + "https://w3id.org/mobilitydcat-ap/mobilityTheme": { + "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category": "Infrastructure and Logistics", + "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category": "General Information About Planning Of Routes" + }, + "https://w3id.org/mobilitydcat-ap/georeferencingMethod": "my-geo-reference-method", + "https://w3id.org/mobilitydcat-ap/transportMode": "my-transport-mode", + "http://purl.org/dc/terms/rightsHolder": "my-sovereign", + "http://purl.org/dc/terms/spatial": { + "http://www.w3.org/2004/02/skos/core#prefLabel": "my-geolocation", + "http://purl.org/dc/terms/identifier": [ + "my-nuts-location1", + "my-nuts-location2" + ] + }, + "http://purl.org/dc/terms/accrualPeriodicity": "my-data-update-frequency", + "http://purl.org/dc/terms/temporal": { + "http://www.w3.org/ns/dcat#startDate": "2007-12-03", + "http://www.w3.org/ns/dcat#endDate": "2024-01-22" + }, + "https://semantic.sovity.io/dcat-ext#customJson": "{\"array\":[3,1,4,1,5],\"boolean\":false,\"null\":null,\"number\":116,\"object\":{\"key\":\"value\"},\"string\":\"value\"}", + "http://unknown/some-custom-string": "some-string-value", + "http://unknown/some-custom-obj": {"http://unknown/a": "b"} + }, + "https://w3id.org/edc/v0.0.1/ns/privateProperties": { + "https://semantic.sovity.io/dcat-ext#privateCustomJson":"{\"priv_array\":[3,1,4,1,5],\"priv_boolean\":false,\"priv_null\":null,\"priv_number\":116,\"priv_object\":{\"key\":\"value\"},\"priv_string\":\"value\"}", + "http://unknown/some-custom-private-string": "some-private-value", + "http://unknown/some-custom-private-obj": {"http://unknown/a-private": "b-private"} + }, + "https://w3id.org/edc/v0.0.1/ns/DataAddress": { + "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "https://data-source.my-org" + } +} diff --git a/extensions/wrapper/wrapper-ee-api/README.md b/extensions/wrapper/wrapper-ee-api/README.md new file mode 100644 index 000000000..0902aa563 --- /dev/null +++ b/extensions/wrapper/wrapper-ee-api/README.md @@ -0,0 +1,30 @@ + +
      +

      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      sovity Enterprise Edition EDC API +Specification

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +Specification of sovity Enterprise Edition EDC API endpoints to be included in API Client generation. + +These endpoints require our sovity Enterprise Edition EDC Extensions. + +## License + +Apache License 2.0 - see [LICENSE](../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/wrapper-ee-api/build.gradle.kts b/extensions/wrapper/wrapper-ee-api/build.gradle.kts new file mode 100644 index 000000000..eab881398 --- /dev/null +++ b/extensions/wrapper/wrapper-ee-api/build.gradle.kts @@ -0,0 +1,33 @@ +val lombokVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api(project(":extensions:wrapper:wrapper-common-api")) + + api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + api("jakarta.validation:jakarta.validation-api:3.0.2") + api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") + api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") + api("jakarta.servlet:jakarta.servlet-api:5.0.0") + + implementation("org.apache.commons:commons-lang3:3.13.0") + implementation("org.glassfish.jersey.media:jersey-media-multipart:3.1.3") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/EnterpriseEditionResource.java b/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/EnterpriseEditionResource.java new file mode 100644 index 000000000..7ce39be7d --- /dev/null +++ b/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/EnterpriseEditionResource.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ee; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.ee.model.ConnectorLimits; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Our sovity Enterprise Edition EDC API Endpoints to be included in our generated EDC API Wrapper Clients + */ +@Path("wrapper/ee") +@Tag(name = "Enterprise Edition", description = "sovity Enterprise Edition EDC API Endpoints. Requires our sovity Enterprise Edition EDC Extensions.") +public interface EnterpriseEditionResource { + @GET + @Path("connector-limits") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Available and used resources of a connector.") + ConnectorLimits connectorLimits(); + + @POST + @Path("file-upload/blobs") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Requests a Blob for file upload.", + description = "Requests a Blob URL with a SAS Token so that the UI can directly upload the file to the Azure Blob Storage. Returns the Blob ID / Token." + ) + String fileUploadRequestSasToken(); + + @POST + @Path("file-upload/blobs/{blobId}/asset") + @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Create an asset from an uploaded file.", + description = "Creates an asset using the uploaded file as data source." + ) + void fileUploadCreateAsset( + @PathParam("blobId") + @Parameter( + name = "blobId", + in = ParameterIn.PATH, + description = "The Blob ID / URL the file was uploaded into." + ) String blobId, + + @Parameter( + required = true, + description = "Metadata for the Asset. File-related metadata might be overridden." + ) UiAssetCreateRequest assetCreateRequest + ); +} diff --git a/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/ConnectorLimits.java b/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/ConnectorLimits.java new file mode 100644 index 000000000..84bf1969a --- /dev/null +++ b/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/ConnectorLimits.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ee.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Available and used resources of a connector.") +public class ConnectorLimits { + @Schema(description = "Current amount of active consuming contract agreements.", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer numActiveConsumingContractAgreements; + + @Schema(description = "Maximum amount of active consuming contract agreements. A value of 'null' or a negative value means that there are no limit.") + private Integer maxActiveConsumingContractAgreements; +} + diff --git a/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/StoredFile.java b/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/StoredFile.java new file mode 100644 index 000000000..d91ada16b --- /dev/null +++ b/extensions/wrapper/wrapper-ee-api/src/main/java/de/sovity/edc/ext/wrapper/api/ee/model/StoredFile.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ee.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.Map; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Represents a stored file in the file storage extension") +public class StoredFile { + @Schema(description = "Identifier of the StoredFile object", + example = "stored-file-001", + requiredMode = Schema.RequiredMode.REQUIRED) + private String storedFileId; + + @Schema(description = "The name of file.", + example = "afilename.csv", + requiredMode = Schema.RequiredMode.REQUIRED) + private String fileName; + + @Schema(description = "The extension of the file.", + example = "csv", + requiredMode = Schema.RequiredMode.REQUIRED) + private String fileExtension; + + @Schema(description = "The media type of the file.", + example = "text/csv", + requiredMode = Schema.RequiredMode.REQUIRED) + private String mediaType; + + @Schema(description = "Size of the file in bytes.", + example = "1024", + requiredMode = Schema.RequiredMode.REQUIRED) + private String byteSize; + + @Schema(description = "Map containing the asset properties of the stored file." + + "
      An empty map is set as a response to a file storage request.
      Only upon a asset creation request " + + "the asset properties are set.", + example = "{\"asset:prop:id\": \"some-asset-1\",\n \"asset:prop:originator\": \"http://my-example-connector/api/v1/ids\"}", + requiredMode = Schema.RequiredMode.REQUIRED) + private Map assetProperties; + + @Schema(description = "Creation date of the StoredFile object.", + example = "2023-05-05T12:00:00.000+02:00", + requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime creationDate; + + @Schema(description = "Date of the last modification of the StoredFile object.", + example = "2023-05-05T14:00:00.000+02:00", + requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime lastModifiedDate; +} diff --git a/extensions/wrapper/wrapper/README.md b/extensions/wrapper/wrapper/README.md new file mode 100644 index 000000000..48b5bb007 --- /dev/null +++ b/extensions/wrapper/wrapper/README.md @@ -0,0 +1,40 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Community Edition API Wrapper Implementation

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this component + +EDC extension which provides API implementations for most of the Wrapper API endpoints. Excluded endpoints are either +implemented in other sovity Community Edition EDC Extensions or implemented in our Enterprise Edition. + +## Why does this extension exist? + +The goal is to design APIs so they can be used losslessly with OpenAPI 3.0 generators to provide different client +libraries at ease, while extending or simplifying connector use and functionality. + +Furthermore, not using the often changing Eclipse EDC APIs, but rather our own facade +allows better backwards compatibility for both our UIs and Use Case Applications. + +With the move to JSON-LD both input and output of the Eclipse EDC APIs must be semantically interpreted. +JSON-LD libraries are not available in all languages or well to use, therefore this API Wrapper can alleviate that pain, +providing simple and type-safe REST APIs,. materializing polymorphisms into sum types. + +## License + +Apache License 2.0 - see [LICENSE](../../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts new file mode 100644 index 000000000..40681b945 --- /dev/null +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -0,0 +1,87 @@ +val assertj: String by project +val edcVersion: String by project +val edcGroup: String by project +val jettyGroup: String by project +val jettyVersion: String by project +val jsonUnit: String by project +val lombokVersion: String by project +val mockitoVersion: String by project +val restAssured: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + implementation("${edcGroup}:api-core:${edcVersion}") + implementation("${edcGroup}:management-api-configuration:${edcVersion}") + implementation("${edcGroup}:dsp-http-spi:${edcVersion}") + api(project(":extensions:wrapper:wrapper-api")) + api(project(":extensions:wrapper:wrapper-common-mappers")) + api(project(":utils:catalog-parser")) + api(project(":utils:json-and-jsonld-utils")) + api("${edcGroup}:contract-definition-api:${edcVersion}") + api("${edcGroup}:control-plane-spi:${edcVersion}") + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:policy-definition-api:${edcVersion}") + api("${edcGroup}:transfer-process-api:${edcVersion}") + implementation("org.apache.commons:commons-lang3:3.13.0") + + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + + testImplementation(project(":extensions:wrapper:clients:java-client")) + testImplementation(project(":extensions:policy-always-true")) + testImplementation(project(":utils:test-utils")) + testImplementation("${edcGroup}:control-plane-core:${edcVersion}") + testImplementation("${edcGroup}:dsp:${edcVersion}") + testImplementation("${edcGroup}:iam-mock:${edcVersion}") + testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation("${edcGroup}:http:${edcVersion}") { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation("${jettyGroup}:jetty-client:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-http:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-io:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-server:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-util:${jettyVersion}") + testImplementation("${jettyGroup}:jetty-webapp:${jettyVersion}") + + testImplementation("${edcGroup}:json-ld:${edcVersion}") + testImplementation("${edcGroup}:dsp-http-spi:${edcVersion}") + testImplementation("${edcGroup}:dsp-api-configuration:${edcVersion}") + testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") + + testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") + testImplementation("io.rest-assured:rest-assured:${restAssured}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +tasks.withType { + maxParallelForks = 1 +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java new file mode 100644 index 000000000..82c993a55 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.connector.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.CoreConstants; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.web.spi.WebService; + +public class WrapperExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "WrapperExtension"; + @Inject + private AssetIndex assetIndex; + @Inject + private AssetService assetService; + @Inject + private PolicyDefinitionService policyDefinitionService; + @Inject + private ContractAgreementService contractAgreementService; + @Inject + private ContractDefinitionStore contractDefinitionStore; + @Inject + private ContractNegotiationService contractNegotiationService; + @Inject + private ContractNegotiationStore contractNegotiationStore; + @Inject + private ManagementApiConfiguration dataManagementApiConfiguration; + @Inject + private PolicyDefinitionStore policyDefinitionStore; + @Inject + private PolicyEngine policyEngine; + @Inject + private TransferProcessService transferProcessService; + @Inject + private TransferProcessStore transferProcessStore; + @Inject + private TypeManager typeManager; + @Inject + private ManagementApiTypeTransformerRegistry typeTransformerRegistry; + @Inject + private WebService webService; + @Inject + private ContractDefinitionService contractDefinitionService; + @Inject + private CatalogService catalogService; + @Inject + private JsonLd jsonLd; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var objectMapper = typeManager.getMapper(CoreConstants.JSON_LD); + fixObjectMapperDateSerialization(objectMapper); + + var wrapperExtensionContext = WrapperExtensionContextBuilder.buildContext( + assetIndex, + assetService, + catalogService, + context.getConfig(), + contractAgreementService, + contractDefinitionService, + contractDefinitionStore, + contractNegotiationService, + contractNegotiationStore, + jsonLd, + context.getMonitor(), + objectMapper, + policyDefinitionService, + policyDefinitionStore, + policyEngine, + transferProcessService, + transferProcessStore, + typeTransformerRegistry + ); + + wrapperExtensionContext.selfDescriptionService().validateSelfDescriptionConfig(); + + wrapperExtensionContext.jaxRsResources().forEach(resource -> + webService.registerResource(dataManagementApiConfiguration.getContextAlias(), resource)); + } + + private void fixObjectMapperDateSerialization(ObjectMapper objectMapper) { + // Fixes Dates in JSON-LD Object Mapper + // The Core EDC uses longs over OffsetDateTime, so they never fixed the date format + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java new file mode 100644 index 000000000..7280df3b0 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper; + +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; + +import java.util.List; + + +/** + * Manual Dependency Injection result + * + * @param jaxRsResources Jax RS Resource implementations to register. Implementations of + * APIs supported by our EDC API Client that don't have their own + * extension should land here. + * @param selfDescriptionService Required here for validation on start-up + */ +public record WrapperExtensionContext( + List jaxRsResources, + SelfDescriptionService selfDescriptionService +) { +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java new file mode 100644 index 000000000..4721d5a3f --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.ui.UiResourceImpl; +import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder; +import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetIdValidator; +import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.CatalogApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTransferApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementDataFetcher; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementPageCardBuilder; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementUtils; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractNegotiationUtils; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.TransferRequestBuilder; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionBuilder; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.CriterionLiteralMapper; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.CriterionMapper; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.CriterionOperatorMapper; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractNegotiationApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractNegotiationBuilder; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractNegotiationStateService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractOfferMapper; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.DashboardPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.DapsConfigService; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.DashboardDataFetcher; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.MiwConfigService; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.OwnConnectorEndpointServiceImpl; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; +import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; +import de.sovity.edc.ext.wrapper.api.usecase.UseCaseResourceImpl; +import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; +import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; +import de.sovity.edc.utils.catalog.DspCatalogService; +import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; +import lombok.NoArgsConstructor; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.connector.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.util.List; + + +/** + * Manual Dependency Injection. + *

      + * We want to develop as Java Backend Development is done, but we have no CDI / DI Framework to rely + * on. + *

      + * EDC {@link Inject} only works in {@link WrapperExtension}. + */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class WrapperExtensionContextBuilder { + + public static WrapperExtensionContext buildContext( + AssetIndex assetIndex, + AssetService assetService, + CatalogService catalogService, + Config config, + ContractAgreementService contractAgreementService, + ContractDefinitionService contractDefinitionService, + ContractDefinitionStore contractDefinitionStore, + ContractNegotiationService contractNegotiationService, + ContractNegotiationStore contractNegotiationStore, + JsonLd jsonLd, + Monitor monitor, + ObjectMapper objectMapper, + PolicyDefinitionService policyDefinitionService, + PolicyDefinitionStore policyDefinitionStore, + PolicyEngine policyEngine, + TransferProcessService transferProcessService, + TransferProcessStore transferProcessStore, + TypeTransformerRegistry typeTransformerRegistry + ) { + // UI API + var operatorMapper = new OperatorMapper(); + var criterionOperatorMapper = new CriterionOperatorMapper(); + var criterionLiteralMapper = new CriterionLiteralMapper(); + var criterionMapper = new CriterionMapper(criterionOperatorMapper, criterionLiteralMapper); + var literalMapper = new LiteralMapper(objectMapper); + var atomicConstraintMapper = new AtomicConstraintMapper(literalMapper, operatorMapper); + var policyValidator = new PolicyValidator(); + var constraintExtractor = new ConstraintExtractor(policyValidator, atomicConstraintMapper); + var policyMapper = new PolicyMapper( + constraintExtractor, + atomicConstraintMapper, + typeTransformerRegistry); + var edcPropertyUtils = new EdcPropertyUtils(); + var assetJsonLdUtils = new AssetJsonLdUtils(); + var markdownToTextConverter = new MarkdownToTextConverter(); + var textUtils = new TextUtils(); + var selfDescriptionService = new SelfDescriptionService(config, monitor); + var ownConnectorEndpointService = new OwnConnectorEndpointServiceImpl(selfDescriptionService); + var uiAssetMapper = new UiAssetMapper(edcPropertyUtils, assetJsonLdUtils, markdownToTextConverter, textUtils, ownConnectorEndpointService); + var assetMapper = new AssetMapper(typeTransformerRegistry, uiAssetMapper, jsonLd); + var transferProcessStateService = new TransferProcessStateService(); + var contractNegotiationUtils = new ContractNegotiationUtils( + contractNegotiationService, + selfDescriptionService + ); + var contractAgreementPageCardBuilder = new ContractAgreementPageCardBuilder( + policyMapper, + transferProcessStateService, + assetMapper, + contractNegotiationUtils + ); + var contractAgreementDataFetcher = new ContractAgreementDataFetcher( + contractAgreementService, + contractNegotiationStore, + transferProcessService, + assetIndex + ); + var contractAgreementApiService = new ContractAgreementPageApiService( + contractAgreementDataFetcher, + contractAgreementPageCardBuilder + ); + var contactDefinitionBuilder = new ContractDefinitionBuilder(criterionMapper); + var contractDefinitionApiService = new ContractDefinitionApiService( + contractDefinitionService, + criterionMapper, + contactDefinitionBuilder); + var transferHistoryPageApiService = new TransferHistoryPageApiService( + assetService, + contractAgreementService, + contractNegotiationStore, + transferProcessService, + transferProcessStateService + ); + var transferHistoryPageAssetFetcherService = new TransferHistoryPageAssetFetcherService( + assetService, + transferProcessService, + assetMapper, + contractNegotiationStore, + contractNegotiationUtils + ); + var contractAgreementUtils = new ContractAgreementUtils(contractAgreementService); + var parameterizationCompatibilityUtils = new ParameterizationCompatibilityUtils(); + var assetIdValidator = new AssetIdValidator(); + var assetBuilder = new AssetBuilder( + assetMapper, + edcPropertyUtils, + assetIdValidator, + selfDescriptionService + ); + var assetApiService = new AssetApiService( + assetService, + assetMapper, + assetBuilder, + selfDescriptionService + ); + var transferRequestBuilder = new TransferRequestBuilder( + contractAgreementUtils, + contractNegotiationUtils, + edcPropertyUtils, + typeTransformerRegistry, + parameterizationCompatibilityUtils + ); + var contractAgreementTransferApiService = new ContractAgreementTransferApiService( + transferRequestBuilder, + transferProcessService + ); + var policyDefinitionApiService = new PolicyDefinitionApiService( + policyDefinitionService, + policyMapper + ); + var dataOfferBuilder = new DspDataOfferBuilder(jsonLd); + var dspCatalogService = new DspCatalogService(catalogService, dataOfferBuilder); + var catalogApiService = new CatalogApiService( + assetMapper, + policyMapper, + dspCatalogService + ); + var contractOfferMapper = new ContractOfferMapper(policyMapper); + var contractNegotiationBuilder = new ContractNegotiationBuilder(contractOfferMapper); + var contractNegotiationStateService = new ContractNegotiationStateService(); + var contractNegotiationApiService = new ContractNegotiationApiService( + contractNegotiationService, + contractNegotiationBuilder, + contractNegotiationStateService + ); + var miwConfigBuilder = new MiwConfigService(config); + var dapsConfigBuilder = new DapsConfigService(config); + var dashboardDataFetcher = new DashboardDataFetcher( + contractNegotiationStore, + transferProcessService, + assetIndex, + policyDefinitionService, + contractDefinitionService + ); + var dashboardApiService = new DashboardPageApiService( + dashboardDataFetcher, + transferProcessStateService, + dapsConfigBuilder, + miwConfigBuilder, + selfDescriptionService + ); + var uiResource = new UiResourceImpl( + contractAgreementApiService, + contractAgreementTransferApiService, + transferHistoryPageApiService, + transferHistoryPageAssetFetcherService, + assetApiService, + policyDefinitionApiService, + catalogApiService, + contractDefinitionApiService, + contractNegotiationApiService, + dashboardApiService + ); + + // Use Case API + var kpiApiService = new KpiApiService( + assetIndex, + policyDefinitionStore, + contractDefinitionStore, + transferProcessStore, + contractAgreementService, + transferProcessStateService + ); + var supportedPolicyApiService = new SupportedPolicyApiService(policyEngine); + var useCaseResource = new UseCaseResourceImpl( + kpiApiService, + supportedPolicyApiService + ); + + // Collect all JAX-RS resources + return new WrapperExtensionContext(List.of( + uiResource, + useCaseResource + ), selfDescriptionService); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java new file mode 100644 index 000000000..115ab8643 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; + +@OpenAPIDefinition( + info = @Info( + title = "sovity EDC API Wrapper", + version = "0.0.0", + description = "sovity's EDC API Wrapper contains a selection of APIs for multiple consumers, " + + "e.g. our EDC UI API, our generic Use Case API, our Commercial APIs, etc. " + + "We bundled these APIs, so we can have an easier time generating our API Client Libraries.", + contact = @Contact( + name = "Sovity GmbH", + email = "contact@sovity.de", + url = "https://github.com/sovity/edc-extensions/issues/new/choose" + ), + license = @License( + name = "Apache 2.0", + url = "https://github.com/sovity/edc-extensions/blob/main/LICENSE" + ) + ), + externalDocs = @ExternalDocumentation( + description = "EDC API Wrapper Project in sovity/edc-extensions", + url = "https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper" + ) +) +public interface ApiInformation { +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ServiceException.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ServiceException.java new file mode 100644 index 000000000..3a4af30d5 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ServiceException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api; + +import jakarta.ws.rs.WebApplicationException; +import org.eclipse.edc.service.spi.result.ServiceResult; +import org.eclipse.edc.spi.result.Failure; + +import java.util.function.Function; + +/** + * Exception for handling {@link ServiceResult} {@link Failure}s. + * + * @see ServiceResult#orElseThrow(Function) + */ +public class ServiceException extends WebApplicationException { + public ServiceException(Failure failure) { + super(failure.getFailureDetail(), 500); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java new file mode 100644 index 000000000..aad1111ac --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui; + +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionPage; +import de.sovity.edc.ext.wrapper.api.ui.model.TransferHistoryPage; +import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.CatalogApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTransferApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractNegotiationApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.DashboardPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@SuppressWarnings("java:S6539") // This class is so large so the generated API Clients can have one UiApi +@RequiredArgsConstructor +public class UiResourceImpl implements UiResource { + + private final ContractAgreementPageApiService contractAgreementApiService; + private final ContractAgreementTransferApiService contractAgreementTransferApiService; + private final TransferHistoryPageApiService transferHistoryPageApiService; + private final TransferHistoryPageAssetFetcherService transferHistoryPageAssetFetcherService; + private final AssetApiService assetApiService; + private final PolicyDefinitionApiService policyDefinitionApiService; + private final CatalogApiService catalogApiService; + private final ContractDefinitionApiService contractDefinitionApiService; + private final ContractNegotiationApiService contractNegotiationApiService; + private final DashboardPageApiService dashboardPageApiService; + + @Override + public DashboardPage getDashboardPage() { + return dashboardPageApiService.dashboardPage(); + } + + @Override + public AssetPage getAssetPage() { + return new AssetPage(assetApiService.getAssets()); + } + + @Override + public IdResponseDto createAsset(UiAssetCreateRequest uiAssetCreateRequest) { + return assetApiService.createAsset(uiAssetCreateRequest); + } + + @Override + public IdResponseDto editAssetMetadata(String assetId, UiAssetEditMetadataRequest uiAssetEditMetadataRequest) { + return assetApiService.editAsset(assetId, uiAssetEditMetadataRequest); + } + + @Override + public IdResponseDto deleteAsset(String assetId) { + return assetApiService.deleteAsset(assetId); + } + + @Override + public PolicyDefinitionPage getPolicyDefinitionPage() { + return new PolicyDefinitionPage(policyDefinitionApiService.getPolicyDefinitions()); + } + + @Override + public IdResponseDto createPolicyDefinition(PolicyDefinitionCreateRequest policyDefinitionDtoDto) { + return policyDefinitionApiService.createPolicyDefinition(policyDefinitionDtoDto); + } + + @Override + public IdResponseDto deletePolicyDefinition(String policyId) { + return policyDefinitionApiService.deletePolicyDefinition(policyId); + } + + @Override + public ContractDefinitionPage getContractDefinitionPage() { + return new ContractDefinitionPage(contractDefinitionApiService.getContractDefinitions()); + } + + @Override + public IdResponseDto createContractDefinition(ContractDefinitionRequest contractDefinitionRequest) { + return contractDefinitionApiService.createContractDefinition(contractDefinitionRequest); + } + + @Override + public IdResponseDto deleteContractDefinition(String contractDefinitionId) { + return contractDefinitionApiService.deleteContractDefinition(contractDefinitionId); + } + + @Override + public List getCatalogPageDataOffers(String connectorEndpoint) { + return catalogApiService.fetchDataOffers(connectorEndpoint); + } + + @Override + public UiContractNegotiation initiateContractNegotiation(ContractNegotiationRequest contractNegotiationRequest) { + return contractNegotiationApiService.initiateContractNegotiation(contractNegotiationRequest); + } + + @Override + public UiContractNegotiation getContractNegotiation(String contractNegotiationId) { + return contractNegotiationApiService.getContractNegotiation(contractNegotiationId); + } + + @Override + public ContractAgreementPage getContractAgreementPage() { + return contractAgreementApiService.contractAgreementPage(); + } + + @Override + public IdResponseDto initiateTransfer(InitiateTransferRequest request) { + return contractAgreementTransferApiService.initiateTransfer(request); + } + + @Override + public IdResponseDto initiateCustomTransfer(InitiateCustomTransferRequest request) { + return contractAgreementTransferApiService.initiateCustomTransfer(request); + } + + @Override + public TransferHistoryPage getTransferHistoryPage() { + return new TransferHistoryPage(transferHistoryPageApiService.getTransferHistoryEntries()); + } + + @Override + public UiAsset getTransferProcessAsset(String transferProcessId) { + return transferHistoryPageAssetFetcherService.getAssetForTransferHistoryPage(transferProcessId); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java new file mode 100644 index 000000000..3e3206015 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.asset; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +public class AssetApiService { + private final AssetService assetService; + private final AssetMapper assetMapper; + private final AssetBuilder assetBuilder; + private final SelfDescriptionService selfDescriptionService; + + public List getAssets() { + var assets = getAllAssets(); + var connectorEndpoint = selfDescriptionService.getConnectorEndpoint(); + var participantId = selfDescriptionService.getParticipantId(); + return assets.stream().sorted(Comparator.comparing(Asset::getCreatedAt).reversed()) + .map(asset -> assetMapper.buildUiAsset(asset, connectorEndpoint, participantId)) + .toList(); + } + + @NotNull + public IdResponseDto createAsset(UiAssetCreateRequest request) { + val asset = assetBuilder.fromCreateRequest(request); + val createdAsset = assetService.create(asset).orElseThrow(ServiceException::new); + return new IdResponseDto(createdAsset.getId()); + } + + public IdResponseDto editAsset(String assetId, UiAssetEditMetadataRequest request) { + val foundAsset = assetService.findById(assetId); + Objects.requireNonNull(foundAsset, "Asset with ID %s not found".formatted(assetId)); + val editedAsset = assetBuilder.fromEditMetadataRequest(foundAsset, request); + val updatedAsset = assetService.update(editedAsset).orElseThrow(ServiceException::new); + return new IdResponseDto(updatedAsset.getId()); + } + + @NotNull + public IdResponseDto deleteAsset(String assetId) { + var response = assetService.delete(assetId).orElseThrow(ServiceException::new); + return new IdResponseDto(response.getId()); + } + + private List getAllAssets() { + return assetService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java new file mode 100644 index 000000000..7eccc608f --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.asset; + +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.types.domain.asset.Asset; + +@RequiredArgsConstructor +public class AssetBuilder { + private final AssetMapper assetMapper; + private final EdcPropertyUtils edcPropertyUtils; + private final AssetIdValidator assetIdValidator; + private final SelfDescriptionService selfDescriptionService; + + /** + * Creates an {@link Asset} from a {@link UiAssetCreateRequest}. + * + * @param request {@link UiAssetCreateRequest} + * @return {@link Asset} + */ + public Asset fromCreateRequest(UiAssetCreateRequest request) { + assetIdValidator.assertValid(request.getId()); + var organizationName = selfDescriptionService.getCuratorName(); + return assetMapper.buildAsset(request, organizationName); + } + + /** + * Returns an edited copy of an {@link Asset} updated with the given {@link UiAssetEditMetadataRequest} + * + * @param asset {@link Asset} (immutable) + * @param request {@link UiAssetEditMetadataRequest} + * @return copy of {@link Asset} + */ + public Asset fromEditMetadataRequest(Asset asset, UiAssetEditMetadataRequest request) { + var createRequest = buildCreateRequest(asset, request); + var tmpAsset = fromCreateRequest(createRequest); + + // DEBT: On each EDC update, check that no field was added in + // org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder + return Asset.Builder.newInstance() + .id(asset.getId()) + .properties(tmpAsset.getProperties()) + .privateProperties(tmpAsset.getPrivateProperties()) + .dataAddress(asset.getDataAddress()) + .createdAt(asset.getCreatedAt()) + .build(); + } + + + private UiAssetCreateRequest buildCreateRequest(Asset asset, UiAssetEditMetadataRequest editRequest) { + var dataAddress = edcPropertyUtils.truncateToMapOfString(asset.getDataAddress().getProperties()); + + var createRequest = new UiAssetCreateRequest(); + createRequest.setId(asset.getId()); + createRequest.setConditionsForUse(editRequest.getConditionsForUse()); + createRequest.setCustomJsonAsString(editRequest.getCustomJsonAsString()); + createRequest.setCustomJsonLdAsString(editRequest.getCustomJsonLdAsString()); + createRequest.setDataAddressProperties(dataAddress); + createRequest.setDataCategory(editRequest.getDataCategory()); + createRequest.setDataModel(editRequest.getDataModel()); + createRequest.setDataSampleUrls(editRequest.getDataSampleUrls()); + createRequest.setDataSubcategory(editRequest.getDataSubcategory()); + createRequest.setDataUpdateFrequency(editRequest.getDataUpdateFrequency()); + createRequest.setDescription(editRequest.getDescription()); + createRequest.setGeoLocation(editRequest.getGeoLocation()); + createRequest.setGeoReferenceMethod(editRequest.getGeoReferenceMethod()); + createRequest.setKeywords(editRequest.getKeywords()); + createRequest.setLandingPageUrl(editRequest.getLandingPageUrl()); + createRequest.setLanguage(editRequest.getLanguage()); + createRequest.setLicenseUrl(editRequest.getLicenseUrl()); + createRequest.setMediaType(editRequest.getMediaType()); + createRequest.setNutsLocations(editRequest.getNutsLocations()); + createRequest.setPrivateCustomJsonAsString(editRequest.getPrivateCustomJsonAsString()); + createRequest.setPrivateCustomJsonLdAsString(editRequest.getPrivateCustomJsonLdAsString()); + createRequest.setPublisherHomepage(editRequest.getPublisherHomepage()); + createRequest.setReferenceFileUrls(editRequest.getReferenceFileUrls()); + createRequest.setReferenceFilesDescription(editRequest.getReferenceFilesDescription()); + createRequest.setSovereignLegalName(editRequest.getSovereignLegalName()); + createRequest.setTemporalCoverageFrom(editRequest.getTemporalCoverageFrom()); + createRequest.setTemporalCoverageToInclusive(editRequest.getTemporalCoverageToInclusive()); + createRequest.setTitle(editRequest.getTitle()); + createRequest.setTransportMode(editRequest.getTransportMode()); + createRequest.setVersion(editRequest.getVersion()); + + return createRequest; + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidator.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidator.java new file mode 100644 index 000000000..7c0434cc3 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidator.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.asset; + +import org.apache.commons.lang3.Validate; + +import java.util.regex.Pattern; + +public class AssetIdValidator { + private final Pattern pattern = Pattern.compile("^[^\\s:]+$"); + + public boolean isValid(String assetId) { + return pattern.matcher(assetId).matches(); + } + + public void assertValid(String assetId) { + Validate.isTrue(isValid(assetId), "Asset ID must not contain colons or whitespaces."); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java new file mode 100644 index 000000000..c5d27f601 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import de.sovity.edc.ext.wrapper.api.ui.model.UiContractOffer; +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.utils.catalog.DspCatalogService; +import de.sovity.edc.utils.catalog.model.DspContractOffer; +import de.sovity.edc.utils.catalog.model.DspDataOffer; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class CatalogApiService { + private final AssetMapper assetMapper; + private final PolicyMapper policyMapper; + private final DspCatalogService dspCatalogService; + + public List fetchDataOffers(String connectorEndpoint) { + var dspCatalog = dspCatalogService.fetchDataOffers(connectorEndpoint); + var endpoint = dspCatalog.getEndpoint(); + var participantId = dspCatalog.getParticipantId(); + + return dspCatalog.getDataOffers().stream() + .map(dataOffer -> buildDataOffer(dataOffer, endpoint, participantId)) + .toList(); + } + + private UiDataOffer buildDataOffer(DspDataOffer dataOffer, String endpoint, String participantId) { + var uiDataOffer = new UiDataOffer(); + uiDataOffer.setEndpoint(endpoint); + uiDataOffer.setParticipantId(participantId); + uiDataOffer.setAsset(buildUiAsset(dataOffer, endpoint, participantId)); + uiDataOffer.setContractOffers(buildContractOffers(dataOffer.getContractOffers())); + return uiDataOffer; + } + + private List buildContractOffers(List contractOffers) { + return contractOffers.stream().map(this::buildContractOffer).toList(); + } + + private UiContractOffer buildContractOffer(DspContractOffer contractOffer) { + var uiContractOffer = new UiContractOffer(); + uiContractOffer.setContractOfferId(contractOffer.getContractOfferId()); + uiContractOffer.setPolicy(buildUiPolicy(contractOffer)); + return uiContractOffer; + } + + private UiAsset buildUiAsset(DspDataOffer dataOffer, String endpoint, String participantId) { + var asset = assetMapper.buildAssetFromDatasetProperties(dataOffer.getAssetPropertiesJsonLd()); + return assetMapper.buildUiAsset(asset, endpoint, participantId); + } + + private UiPolicy buildUiPolicy(DspContractOffer contractOffer) { + var policy = policyMapper.buildPolicy(contractOffer.getPolicyJsonLd()); + return policyMapper.buildUiPolicy(policy); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java new file mode 100644 index 000000000..8487f8eab --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements; + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementDataFetcher; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementPageCardBuilder; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; + +@RequiredArgsConstructor +public class ContractAgreementPageApiService { + private final ContractAgreementDataFetcher contractAgreementDataFetcher; + private final ContractAgreementPageCardBuilder contractAgreementPageCardBuilder; + + @NotNull + public ContractAgreementPage contractAgreementPage() { + var agreements = contractAgreementDataFetcher.getContractAgreements(); + + var cards = agreements.stream() + .map(agreement -> contractAgreementPageCardBuilder.buildContractAgreementCard( + agreement.agreement(), agreement.negotiation(), agreement.asset(), agreement.transfers())) + .sorted(Comparator.comparing(ContractAgreementCard::getContractSigningDate).reversed()) + .toList(); + + return new ContractAgreementPage(cards); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java new file mode 100644 index 000000000..3f49bd642 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements; + +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.TransferRequestBuilder; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.transfer.spi.types.TransferRequest; +import org.jetbrains.annotations.NotNull; + +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; + +@RequiredArgsConstructor +public class ContractAgreementTransferApiService { + private final TransferRequestBuilder transferRequestBuilder; + private final TransferProcessService transferProcessService; + + @NotNull + public IdResponseDto initiateTransfer(InitiateTransferRequest request) { + var transferRequest = transferRequestBuilder.buildCustomTransferRequest(request); + return initiate(transferRequest); + } + + @NotNull + public IdResponseDto initiateCustomTransfer(InitiateCustomTransferRequest request) { + var transferRequest = transferRequestBuilder.buildCustomTransferRequest(request); + return initiate(transferRequest); + } + + @NotNull + private IdResponseDto initiate(TransferRequest transferRequest) { + var transferProcess = transferProcessService.initiateTransfer(transferRequest) + .orElseThrow(exceptionMapper(TransferProcess.class, transferRequest.getId())); + return new IdResponseDto(transferProcess.getId()); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java new file mode 100644 index 000000000..77f26eefa --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.types.domain.asset.Asset; + +import java.util.List; + + +/** + * Data for a contract agreement as required by the contract agreement page. + * + * @param agreement contract agreement + * @param negotiation contract negotiation + * @param asset asset + * @param transfers transfer processes + */ +public record ContractAgreementData( + ContractAgreement agreement, + ContractNegotiation negotiation, + Asset asset, + List transfers +) { + +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java new file mode 100644 index 000000000..4e9b19e15 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.utils.MapUtils; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.groupingBy; + +@RequiredArgsConstructor +public class ContractAgreementDataFetcher { + private final ContractAgreementService contractAgreementService; + private final ContractNegotiationStore contractNegotiationStore; + private final TransferProcessService transferProcessService; + private final AssetIndex assetIndex; + + /** + * Fetches all contract agreements as {@link ContractAgreementData}s. + * + * @return {@link ContractAgreementData}s + */ + @NotNull + public List getContractAgreements() { + var agreements = getAllContractAgreements(); + var assets = MapUtils.associateBy(getAllAssets(), Asset::getId); + + var negotiations = getAllContractNegotiations().stream() + .filter(it -> it.getContractAgreement() != null) + .collect(groupingBy(it -> it.getContractAgreement().getId())); + + var transfers = getAllTransferProcesses().stream() + .collect(groupingBy(it -> it.getDataRequest().getContractId())); + + // A ContractAgreement has multiple ContractNegotiations when doing a loopback consumption + return agreements.stream() + .flatMap(agreement -> negotiations.getOrDefault(agreement.getId(), List.of()).stream() + .map(negotiation -> { + var asset = getAsset(agreement, negotiation, assets); + var contractTransfers = transfers.getOrDefault(agreement.getId(), List.of()); + return new ContractAgreementData(agreement, negotiation, asset, contractTransfers); + })) + .toList(); + } + + private Asset getAsset(ContractAgreement agreement, ContractNegotiation negotiation, Map assets) { + var assetId = agreement.getAssetId(); + + if (negotiation.getType() == ContractNegotiation.Type.CONSUMER) { + return dummyAsset(assetId); + } + + var asset = assets.get(assetId); + return asset == null ? dummyAsset(assetId) : asset; + } + + private Asset dummyAsset(String assetId) { + return Asset.Builder.newInstance().id(assetId).build(); + } + + private List getAllAssets() { + return assetIndex.queryAssets(QuerySpec.max()).toList(); + } + + @NotNull + private List getAllContractNegotiations() { + return contractNegotiationStore.queryNegotiations(QuerySpec.max()).toList(); + } + + @NotNull + private List getAllContractAgreements() { + return contractAgreementService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } + + @NotNull + private List getAllTransferProcesses() { + return transferProcessService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java new file mode 100644 index 000000000..d090c4401 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementDirection; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTransferProcess; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; + +import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcMillisToOffsetDateTime; +import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcSecondsToOffsetDateTime; + +@RequiredArgsConstructor +public class ContractAgreementPageCardBuilder { + private final PolicyMapper policyMapper; + private final TransferProcessStateService transferProcessStateService; + private final AssetMapper assetMapper; + private final ContractNegotiationUtils contractNegotiationUtils; + + @NotNull + public ContractAgreementCard buildContractAgreementCard( + @NonNull ContractAgreement agreement, + @NonNull ContractNegotiation negotiation, + @NonNull Asset asset, + @NonNull List transferProcesses + ) { + var assetParticipantId = contractNegotiationUtils.getProviderParticipantId(negotiation); + var assetConnectorEndpoint = contractNegotiationUtils.getProviderConnectorEndpoint(negotiation); + + ContractAgreementCard card = new ContractAgreementCard(); + card.setContractAgreementId(agreement.getId()); + card.setContractNegotiationId(negotiation.getId()); + card.setDirection(ContractAgreementDirection.fromType(negotiation.getType())); + card.setCounterPartyAddress(negotiation.getCounterPartyAddress()); + card.setCounterPartyId(negotiation.getCounterPartyId()); + card.setContractSigningDate(utcSecondsToOffsetDateTime(agreement.getContractSigningDate())); + card.setAsset(assetMapper.buildUiAsset(asset, assetConnectorEndpoint, assetParticipantId)); + card.setContractPolicy(policyMapper.buildUiPolicy(agreement.getPolicy())); + card.setTransferProcesses(buildTransferProcesses(transferProcesses)); + return card; + } + + @NotNull + private List buildTransferProcesses( + @NonNull List transferProcessEntities + ) { + return transferProcessEntities.stream() + .map(this::buildContractAgreementTransfer) + .sorted(Comparator.comparing(ContractAgreementTransferProcess::getLastUpdatedDate) + .reversed()) + .toList(); + } + + @NotNull + private ContractAgreementTransferProcess buildContractAgreementTransfer( + TransferProcess transferProcessEntity) { + var transferProcess = new ContractAgreementTransferProcess(); + transferProcess.setTransferProcessId(transferProcessEntity.getId()); + transferProcess.setLastUpdatedDate( + utcMillisToOffsetDateTime(transferProcessEntity.getUpdatedAt())); + transferProcess.setState(transferProcessStateService.buildTransferProcessState( + transferProcessEntity.getState())); + transferProcess.setErrorMessage(transferProcessEntity.getErrorDetail()); + return transferProcess; + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementUtils.java new file mode 100644 index 000000000..9344797e9 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.spi.EdcException; + +import java.util.Optional; + +@RequiredArgsConstructor +public class ContractAgreementUtils { + + private final ContractAgreementService contractAgreementService; + + public ContractAgreement findByIdOrThrow(String contractAgreementId) { + return Optional.ofNullable(contractAgreementService.findById(contractAgreementId)) + .orElseThrow(() -> new EdcException("Could not fetch contractNegotiation for " + + "contractAgreement")); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractNegotiationUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractNegotiationUtils.java new file mode 100644 index 000000000..878053ec3 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractNegotiationUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.util.List; + +@RequiredArgsConstructor +public class ContractNegotiationUtils { + + private final ContractNegotiationService contractNegotiationService; + private final SelfDescriptionService selfDescriptionService; + + public ContractNegotiation findByContractAgreementIdOrThrow(String contractAgreementId) { + var querySpec = QuerySpec.Builder.newInstance() + .filter(List.of(new Criterion("contractAgreement.id", "=", contractAgreementId))) + .build(); + return contractNegotiationService.query(querySpec).orElseThrow(ServiceException::new) + .findFirst() + .orElseThrow(() -> new EdcException("Could not fetch contractNegotiation for " + + "contractAgreement")); + } + + /** + * Return's the asset provider's connector endpoint + * + * @param negotiation negotiation + * @return participant ID + */ + public String getProviderConnectorEndpoint(ContractNegotiation negotiation) { + if (negotiation.getType() == ContractNegotiation.Type.PROVIDER) { + return selfDescriptionService.getConnectorEndpoint(); + } + + return negotiation.getCounterPartyAddress(); + } + + /** + * Return's the asset provider's participant ID + * + * @param negotiation negotiation + * @return participant ID + */ + public String getProviderParticipantId(ContractNegotiation negotiation) { + if (negotiation.getType() == ContractNegotiation.Type.PROVIDER) { + return selfDescriptionService.getParticipantId(); + } + + return negotiation.getCounterPartyId(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java new file mode 100644 index 000000000..4f27d49d4 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.Validate; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.transfer.spi.types.TransferRequest; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +public class TransferRequestBuilder { + private final ContractAgreementUtils contractAgreementUtils; + private final ContractNegotiationUtils contractNegotiationUtils; + private final EdcPropertyUtils edcPropertyUtils; + private final TypeTransformerRegistry typeTransformerRegistry; + private final ParameterizationCompatibilityUtils parameterizationCompatibilityUtils; + + public TransferRequest buildCustomTransferRequest( + InitiateTransferRequest request + ) { + var contractId = request.getContractAgreementId(); + var agreement = contractAgreementUtils.findByIdOrThrow(contractId); + var negotiation = contractNegotiationUtils.findByContractAgreementIdOrThrow(contractId); + var address = parameterizationCompatibilityUtils.enrich(edcPropertyUtils.buildDataAddress(request.getDataSinkProperties()), request.getTransferProcessProperties()); + assertIsConsuming(negotiation); + + return TransferRequest.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .protocol(HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .connectorAddress(negotiation.getCounterPartyAddress()) + .connectorId(negotiation.getCounterPartyId()) + .contractId(contractId) + .assetId(agreement.getAssetId()) + .dataDestination(address) + .privateProperties(edcPropertyUtils.toMapOfObject(request.getTransferProcessProperties())) + .callbackAddresses(List.of()) + .build(); + } + + public TransferRequest buildCustomTransferRequest( + InitiateCustomTransferRequest request + ) { + var contractId = request.getContractAgreementId(); + var agreement = contractAgreementUtils.findByIdOrThrow(contractId); + var negotiation = contractNegotiationUtils.findByContractAgreementIdOrThrow(contractId); + assertIsConsuming(negotiation); + + // Parse Transfer Process JSON-LD + var requestJsonLd = JsonUtils.parseJsonObj(request.getTransferProcessRequestJsonLd()); + + // Expand JSON-LD Property names + requestJsonLd = Json.createObjectBuilder(requestJsonLd) + .add(Prop.TYPE, Prop.Edc.TYPE_TRANSFER_REQUEST) + .add(Prop.CONTEXT, Json.createObjectBuilder(JsonLdUtils.object(requestJsonLd, Prop.CONTEXT)) + .add(Prop.Edc.CTX_ALIAS, Prop.Edc.CTX)) + .build(); + requestJsonLd = JsonLdUtils.expandKeysOnly(requestJsonLd); + + // Add missing properties + requestJsonLd = Json.createObjectBuilder(requestJsonLd) + .add(Prop.TYPE, Prop.Edc.TYPE_TRANSFER_REQUEST) + .add(Prop.Edc.ASSET_ID, agreement.getAssetId()) + .add(Prop.Edc.CONTRACT_ID, agreement.getId()) + .add(Prop.Edc.CONNECTOR_ID, negotiation.getCounterPartyId()) + .add(Prop.Edc.CONNECTOR_ADDRESS, negotiation.getCounterPartyAddress()) + .build(); + + return typeTransformerRegistry.transform(requestJsonLd, TransferRequest.class) + .orElseThrow(ServiceException::new); + } + + private void assertIsConsuming(ContractNegotiation negotiation) { + Validate.isTrue(negotiation.getType() == ContractNegotiation.Type.CONSUMER, + "Agreement is not a consuming agreement."); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionApiService.java new file mode 100644 index 000000000..ca49905b1 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionApiService.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionEntry; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; + + +@RequiredArgsConstructor +public class ContractDefinitionApiService { + private final ContractDefinitionService contractDefinitionService; + private final CriterionMapper criterionMapper; + private final ContractDefinitionBuilder contractDefinitionBuilder; + + public List getContractDefinitions() { + var definitions = getAllContractDefinitions(); + return definitions.stream() + .sorted(Comparator.comparing(ContractDefinition::getCreatedAt).reversed()) + .map(this::buildContractDefinitionEntry) + .toList(); + } + + @NotNull + private ContractDefinitionEntry buildContractDefinitionEntry(ContractDefinition definition) { + var entry = new ContractDefinitionEntry(); + entry.setContractDefinitionId(definition.getId()); + entry.setAccessPolicyId(definition.getAccessPolicyId()); + entry.setContractPolicyId(definition.getContractPolicyId()); + entry.setAssetSelector(criterionMapper.buildUiCriteria(definition.getAssetsSelector())); + return entry; + } + + @NotNull + public IdResponseDto createContractDefinition(ContractDefinitionRequest request) { + var contractDefinition = contractDefinitionBuilder.buildContractDefinition(request); + contractDefinition = contractDefinitionService.create(contractDefinition).orElseThrow(ServiceException::new); + return new IdResponseDto(contractDefinition.getId()); + } + + @NotNull + public IdResponseDto deleteContractDefinition(String contractDefinitionId) { + var response = contractDefinitionService.delete(contractDefinitionId).orElseThrow(ServiceException::new); + return new IdResponseDto(response.getId()); + } + + private List getAllContractDefinitions() { + return contractDefinitionService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionBuilder.java new file mode 100644 index 000000000..b948eed0d --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionBuilder.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; + + +@RequiredArgsConstructor +public class ContractDefinitionBuilder { + private final CriterionMapper criterionMapper; + + public ContractDefinition buildContractDefinition(ContractDefinitionRequest request) { + var contractDefinitionId = request.getContractDefinitionId(); + var contractPolicyId = request.getContractPolicyId(); + var accessPolicyId = request.getAccessPolicyId(); + var assetsSelector = request.getAssetSelector(); + + return ContractDefinition.Builder.newInstance() + .id(contractDefinitionId) + .contractPolicyId(contractPolicyId) + .accessPolicyId(accessPolicyId) + .assetsSelector(criterionMapper.buildCriteria(assetsSelector)) + .build(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapper.java new file mode 100644 index 000000000..b32f8f38e --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteral; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteralType; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class CriterionLiteralMapper { + + public UiCriterionLiteral buildUiCriterionLiteral(Object value) { + if (value instanceof Collection) { + var list = getValueList((Collection) value); + return UiCriterionLiteral.ofValueList(list); + } + + return UiCriterionLiteral.ofValue(value.toString()); + } + + public Object getValue(UiCriterionLiteral dto) { + return switch (dto.getType()) { + case VALUE -> dto.getValue(); + case VALUE_LIST -> dto.getValueList(); + default -> throw new IllegalStateException("Unhandled %s: %s".formatted( + UiCriterionLiteralType.class.getName(), + dto.getType() + )); + }; + } + + private List getValueList(Collection valueList) { + return valueList.stream().map(it -> it == null ? null : it.toString()).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapper.java new file mode 100644 index 000000000..79191ebfd --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterion; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.query.Criterion; + +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +public class CriterionMapper { + private final CriterionOperatorMapper criterionOperatorMapper; + private final CriterionLiteralMapper criterionLiteralMapper; + + public List buildUiCriteria(@NonNull List criteria) { + return criteria.stream().filter(Objects::nonNull).map(this::buildUiCriterion).toList(); + } + + public UiCriterion buildUiCriterion(Criterion criterion) { + var operandLeft = String.valueOf(criterion.getOperandLeft()); + var operator = criterionOperatorMapper.getUiCriterionOperator(criterion.getOperator()); + var operandRight = criterionLiteralMapper.buildUiCriterionLiteral(criterion.getOperandRight()); + + return new UiCriterion(operandLeft, operator, operandRight); + } + + public List buildCriteria(@NonNull List criteria) { + return criteria.stream().filter(Objects::nonNull).map(this::buildCriterion).toList(); + } + + public Criterion buildCriterion(@NonNull UiCriterion criterionDto) { + var operandLeft = criterionDto.getOperandLeft(); + var operator = criterionOperatorMapper.getCriterionOperator(criterionDto.getOperator()); + var operandRight = criterionLiteralMapper.getValue(criterionDto.getOperandRight()); + + return new Criterion(operandLeft, operator, operandRight); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapper.java new file mode 100644 index 000000000..ec153385e --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionOperator; +import de.sovity.edc.ext.wrapper.utils.MapUtils; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +@RequiredArgsConstructor +public class CriterionOperatorMapper { + /** + * @see org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl + */ + private final Map mappings = Map.of( + UiCriterionOperator.EQ, "=", + UiCriterionOperator.LIKE, "like", + UiCriterionOperator.IN, "in" + ); + + private final Map reverseMappings = MapUtils.reverse(mappings); + + public String getCriterionOperator(UiCriterionOperator operator) { + String result = mappings.get(operator); + return requireNonNull(result, () -> "Unhandled %s: %s".formatted( + UiCriterionOperator.class.getName(), operator)); + } + + public UiCriterionOperator getUiCriterionOperator(String operator) { + UiCriterionOperator result = reverseMappings.get(operator == null ? null : operator.toLowerCase()); + return requireNonNull(result, () -> "Could not find %s for: %s".formatted( + UiCriterionOperator.class.getName(), operator)); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationApiService.java new file mode 100644 index 000000000..373318a85 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationApiService.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations; + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.contractnegotiation.ContractNegotiationService; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcMillisToOffsetDateTime; + + +@RequiredArgsConstructor +public class ContractNegotiationApiService { + private final ContractNegotiationService contractNegotiationService; + private final ContractNegotiationBuilder contractNegotiationBuilder; + private final ContractNegotiationStateService contractNegotiationStateService; + + @NotNull + public UiContractNegotiation initiateContractNegotiation(ContractNegotiationRequest request) { + var contractRequest = contractNegotiationBuilder.buildContractNegotiation(request); + var contractNegotiation = contractNegotiationService.initiateNegotiation(contractRequest); + return buildContractNegotiation(contractNegotiation); + } + + @NotNull + public UiContractNegotiation getContractNegotiation(String contractNegotiationId) { + var contractNegotiation = contractNegotiationService.findbyId(contractNegotiationId); + return buildContractNegotiation(contractNegotiation); + } + + @NotNull + private UiContractNegotiation buildContractNegotiation(ContractNegotiation contractNegotiation) { + var status = contractNegotiationStateService.buildContractNegotiationState(contractNegotiation.getState()); + String agreementId = Optional.of(contractNegotiation) + .map(ContractNegotiation::getContractAgreement) + .map(ContractAgreement::getId) + .orElse(null); + return new UiContractNegotiation( + contractNegotiation.getId(), + utcMillisToOffsetDateTime(contractNegotiation.getCreatedAt()), + agreementId, + status + ); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationBuilder.java new file mode 100644 index 000000000..9d098fc00 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations; + + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; + + +@RequiredArgsConstructor +public class ContractNegotiationBuilder { + + private final ContractOfferMapper contractOfferMapper; + + public ContractRequest buildContractNegotiation(ContractNegotiationRequest request) { + var counterPartyAddress = request.getCounterPartyAddress(); + + return ContractRequest.Builder.newInstance() + .counterPartyAddress(counterPartyAddress) + .providerId(request.getCounterPartyParticipantId()) + .protocol(HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .contractOffer(contractOfferMapper.buildContractOffer(request)) + .build(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java new file mode 100644 index 000000000..23d5d2e1c --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations; + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationState; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates; +import org.jetbrains.annotations.NotNull; + + +@RequiredArgsConstructor +public class ContractNegotiationStateService { + + /** + * Interpret {@link org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation#getState()} for use in our UI. + * + * @param code {@link org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation#getState()}, see {@link org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates#code()} + * @return if running + */ + @NotNull + public ContractNegotiationState buildContractNegotiationState(int code) { + var contractNegotiationState = new ContractNegotiationState(); + contractNegotiationState.setCode(code); + contractNegotiationState.setName(getName(code)); + contractNegotiationState.setSimplifiedState(getSimplifiedState(code)); + return contractNegotiationState; + } + + /** + * Which Transfer Process do we want to show as 'running' in our UI? + * + * @param code {@link ContractNegotiation#getState()}, see {@link ContractNegotiationState#code()} + * @return if running + */ + public boolean isRunning(int code) { + // After this there are still states about de-provisioning of resources, + // but we don't really care much about them + return !isError(code) && code < ContractNegotiationStates.FINALIZED.code(); + } + + /** + * Which Transfer Process do we want to show as 'error' in our UI? + * + * @param code {@link ContractNegotiation#getState()}, see {@link ContractNegotiationStates#code()} + * @return if running + */ + public boolean isError(int code) { + return ContractNegotiationStates.TERMINATING.code() == code || ContractNegotiationStates.TERMINATED.code() == code; + } + + @NotNull + private String getName(int code) { + ContractNegotiationStates state = ContractNegotiationStates.from(code); + if (state != null) { + return state.name(); + } + + return "CUSTOM"; + } + + @NotNull + private ContractNegotiationSimplifiedState getSimplifiedState(int code) { + if (isError(code)) { + return ContractNegotiationSimplifiedState.TERMINATED; + } + if (isRunning(code)) { + return ContractNegotiationSimplifiedState.IN_PROGRESS; + } + return ContractNegotiationSimplifiedState.AGREED; + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractOfferMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractOfferMapper.java new file mode 100644 index 000000000..e4a5073bf --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractOfferMapper.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations; + + +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; + + +@RequiredArgsConstructor +public class ContractOfferMapper { + private final PolicyMapper policyMapper; + + public ContractOffer buildContractOffer(ContractNegotiationRequest contractRequest) { + return ContractOffer.Builder.newInstance() + .id(contractRequest.getContractOfferId()) + .policy(policyMapper.buildPolicy(contractRequest.getPolicyJsonLd())) + .assetId(contractRequest.getAssetId()) + .build(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiService.java new file mode 100644 index 000000000..80333f78b --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiService.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard; + +import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.DashboardTransferAmounts; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.DapsConfigService; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.DashboardDataFetcher; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.MiwConfigService; +import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState.ERROR; +import static de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState.OK; +import static de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState.RUNNING; +import static java.util.stream.Collectors.toSet; + +@RequiredArgsConstructor +public class DashboardPageApiService { + private final DashboardDataFetcher dashboardDataFetcher; + private final TransferProcessStateService transferProcessStateService; + private final DapsConfigService dapsConfigService; + private final MiwConfigService miwConfigService; + private final SelfDescriptionService selfDescriptionService; + + @NotNull + public DashboardPage dashboardPage() { + var transferProcesses = dashboardDataFetcher.getTransferProcesses(); + var negotiations = dashboardDataFetcher.getAllContractNegotiations(); + + var providingAgreements = negotiations.stream() + .filter(it -> it.getType() == ContractNegotiation.Type.PROVIDER) + .map(ContractNegotiation::getContractAgreement) + .filter(Objects::nonNull) + .map(ContractAgreement::getId) + .collect(toSet()); + + + var consumingAgreements = negotiations.stream() + .filter(it -> it.getType() == ContractNegotiation.Type.CONSUMER) + .map(ContractNegotiation::getContractAgreement) + .filter(Objects::nonNull) + .map(ContractAgreement::getId) + .collect(toSet()); + + DashboardPage dashboardPage = new DashboardPage(); + dashboardPage.setNumAssets(dashboardDataFetcher.getNumberOfAssets()); + dashboardPage.setNumPolicies(dashboardDataFetcher.getNumberOfPolicies()); + dashboardPage.setNumContractDefinitions(dashboardDataFetcher.getNumberOfContractDefinitions()); + dashboardPage.setNumContractAgreementsProviding(providingAgreements.size()); + dashboardPage.setNumContractAgreementsConsuming(consumingAgreements.size()); + dashboardPage.setTransferProcessesProviding(getTransferAmounts(transferProcesses, providingAgreements)); + dashboardPage.setTransferProcessesConsuming(getTransferAmounts(transferProcesses, consumingAgreements)); + + dashboardPage.setConnectorTitle(selfDescriptionService.getConnectorTitle()); + dashboardPage.setConnectorDescription(selfDescriptionService.getConnectorDescription()); + dashboardPage.setConnectorEndpoint(selfDescriptionService.getConnectorEndpoint()); + dashboardPage.setConnectorParticipantId(selfDescriptionService.getParticipantId()); + + dashboardPage.setConnectorCuratorUrl(selfDescriptionService.getCuratorUrl()); + dashboardPage.setConnectorCuratorName(selfDescriptionService.getCuratorName()); + dashboardPage.setConnectorMaintainerUrl(selfDescriptionService.getMaintainerUrl()); + dashboardPage.setConnectorMaintainerName(selfDescriptionService.getMaintainerName()); + + dashboardPage.setConnectorMiwConfig(miwConfigService.buildMiwConfigOrNull()); + dashboardPage.setConnectorDapsConfig(dapsConfigService.buildDapsConfigOrNull()); + return dashboardPage; + } + + DashboardTransferAmounts getTransferAmounts( + List transferProcesses, + Set agreements + ) { + var numTotal = transferProcesses.stream() + .filter(transferProcess -> agreements.contains(transferProcess.getDataRequest().getContractId())) + .count(); + + var numOk = transferProcesses.stream() + .filter(transferProcess -> agreements.contains(transferProcess.getDataRequest().getContractId())) + .filter(transferProcess -> transferProcessStateService.getSimplifiedState(transferProcess.getState()).equals(OK)) + .count(); + + var numRunning = transferProcesses.stream() + .filter(transferProcess -> agreements.contains(transferProcess.getDataRequest().getContractId())) + .filter(transferProcess -> transferProcessStateService.getSimplifiedState(transferProcess.getState()).equals(RUNNING)) + .count(); + + var numError = transferProcesses.stream() + .filter(transferProcess -> agreements.contains(transferProcess.getDataRequest().getContractId())) + .filter(transferProcess -> transferProcessStateService.getSimplifiedState(transferProcess.getState()).equals(ERROR)) + .count(); + + return new DashboardTransferAmounts(numTotal, numRunning, numOk, numError); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/ConfigPropertyUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/ConfigPropertyUtils.java new file mode 100644 index 000000000..a38d65dde --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/ConfigPropertyUtils.java @@ -0,0 +1,18 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ConfigPropertyUtils { + + /** + * Maps a {@literal CONFIG_KEY} to {@literal config.key} + * + * @param envVarKey {@literal CONFIG_KEY} + * @return {@literal config.key} + */ + public static String configKey(String envVarKey) { + return String.join(".", envVarKey.split("_")).toLowerCase(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DapsConfigService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DapsConfigService.java new file mode 100644 index 000000000..5d1608d42 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DapsConfigService.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; + +import de.sovity.edc.ext.wrapper.api.ui.model.DashboardDapsConfig; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.system.configuration.Config; + +import static com.apicatalog.jsonld.StringUtils.isBlank; +import static de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.ConfigPropertyUtils.configKey; + +@RequiredArgsConstructor +public class DapsConfigService { + private final Config config; + + private static final String DAPS_TOKEN_URL = configKey("EDC_OAUTH_TOKEN_URL"); + + private static final String DAPS_JWKS_URL = configKey("EDC_OAUTH_PROVIDER_JWKS_URL"); + + public DashboardDapsConfig buildDapsConfigOrNull() { + var dapsConfig = new DashboardDapsConfig(); + dapsConfig.setTokenUrl(configValue(DAPS_TOKEN_URL)); + dapsConfig.setJwksUrl(configValue(DAPS_JWKS_URL)); + return isBlank(dapsConfig.getTokenUrl()) ? null : dapsConfig; + } + + String configValue(String configKey) { + return config.getString(configKey, ""); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DashboardDataFetcher.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DashboardDataFetcher.java new file mode 100644 index 000000000..0eee91dbc --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/DashboardDataFetcher.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.query.QuerySpec; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@RequiredArgsConstructor +public class DashboardDataFetcher { + private final ContractNegotiationStore contractNegotiationStore; + private final TransferProcessService transferProcessService; + private final AssetIndex assetIndex; + private final PolicyDefinitionService policyDefinitionService; + private final ContractDefinitionService contractDefinitionService; + + public int getNumberOfAssets() { + return assetIndex.queryAssets(QuerySpec.max()).toList().size(); + } + + public int getNumberOfPolicies() { + return Math.toIntExact(policyDefinitionService.query(QuerySpec.max()) + .orElseThrow(ServiceException::new) + .count()); + } + + public int getNumberOfContractDefinitions() { + return Math.toIntExact(contractDefinitionService.query(QuerySpec.max()) + .orElseThrow(ServiceException::new) + .count()); + } + + @NotNull + public List getAllContractNegotiations() { + return contractNegotiationStore.queryNegotiations(QuerySpec.max()).toList(); + } + @NotNull + public List getTransferProcesses() { + return transferProcessService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/MiwConfigService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/MiwConfigService.java new file mode 100644 index 000000000..8bab54198 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/MiwConfigService.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; + +import de.sovity.edc.ext.wrapper.api.ui.model.DashboardMiwConfig; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.system.configuration.Config; + +import static com.apicatalog.jsonld.StringUtils.isBlank; +import static de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.ConfigPropertyUtils.configKey; + +@RequiredArgsConstructor +public class MiwConfigService { + private final Config config; + + private static final String MIW_AUTHORITY_ID = configKey("TX_SSI_MIW_AUTHORITY_ID"); + + private static final String MIW_URL = configKey("TX_SSI_MIW_URL"); + + private static final String MIW_TOKEN_URL = configKey("TX_SSI_OAUTH_TOKEN_URL"); + + public DashboardMiwConfig buildMiwConfigOrNull() { + var miwConfig = new DashboardMiwConfig(); + miwConfig.setUrl(configValue(MIW_URL)); + miwConfig.setAuthorityId(configValue(MIW_AUTHORITY_ID)); + miwConfig.setTokenUrl(configValue(MIW_TOKEN_URL)); + return isBlank(miwConfig.getUrl()) ? null : miwConfig; + } + + String configValue(String configKey) { + return config.getString(configKey, ""); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java new file mode 100644 index 000000000..b0f7ec1de --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; + +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.OwnConnectorEndpointService; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class OwnConnectorEndpointServiceImpl implements OwnConnectorEndpointService { + private final SelfDescriptionService selfDescriptionService; + + @Override + public boolean isOwnConnectorEndpoint(String endpoint) { + return selfDescriptionService.getConnectorEndpoint().equals(endpoint); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/SelfDescriptionService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/SelfDescriptionService.java new file mode 100644 index 000000000..eb318a271 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/SelfDescriptionService.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.util.List; +import java.util.Objects; + +import static de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.ConfigPropertyUtils.configKey; + +@RequiredArgsConstructor +public class SelfDescriptionService { + private final Config config; + private final Monitor monitor; + private static final String PARTICIPANT_ID = configKey("MY_EDC_PARTICIPANT_ID"); + private static final String CONNECTOR_ENDPOINT = configKey("EDC_DSP_CALLBACK_ADDRESS"); + private static final String TITLE = configKey("MY_EDC_TITLE"); + private static final String DESCRIPTION = configKey("MY_EDC_DESCRIPTION"); + private static final String CURATOR_URL = configKey("MY_EDC_CURATOR_URL"); + private static final String CURATOR_NAME = configKey("MY_EDC_CURATOR_NAME"); + private static final String MAINTAINER_URL = configKey("MY_EDC_MAINTAINER_URL"); + private static final String MAINTAINER_NAME = configKey("MY_EDC_MAINTAINER_NAME"); + private static final List REQUIRED = List.of( + PARTICIPANT_ID, + CONNECTOR_ENDPOINT, + TITLE, + DESCRIPTION, + CURATOR_URL, + CURATOR_NAME, + MAINTAINER_URL, + MAINTAINER_NAME + ); + + /** + * Eclipse EDC Participant ID. Prefer setting {@link #PARTICIPANT_ID} when configuring the connector. + */ + private static final String ECLIPSE_EDC_PARTICIPANT_ID = configKey("EDC_PARTICIPANT_ID"); + + /** + * Deprecated Connector ID configuration property. + *

      + * Required for printing out a warning if still set. + * + * @deprecated Use {@link #PARTICIPANT_ID} instead. + */ + @Deprecated(forRemoval = true) + private static final String NAME_KEBAB_CASE = configKey("MY_EDC_NAME_KEBAB_CASE"); + + public String getParticipantId() { + return configValue(PARTICIPANT_ID); + } + + public String getConnectorEndpoint() { + return configValue(CONNECTOR_ENDPOINT); + } + + public String getConnectorTitle() { + return configValue(TITLE); + } + + public String getConnectorDescription() { + return configValue(DESCRIPTION); + } + + public String getCuratorUrl() { + return configValue(CURATOR_URL); + } + + public String getCuratorName() { + return configValue(CURATOR_NAME); + } + + public String getMaintainerUrl() { + return configValue(MAINTAINER_URL); + } + + public String getMaintainerName() { + return configValue(MAINTAINER_NAME); + } + + public void validateSelfDescriptionConfig() { + var missing = REQUIRED.stream() + .filter(key -> StringUtils.isBlank(configValue(key))) + .toList(); + Validate.isTrue(missing.isEmpty(), + "Missing required configuration properties: %s".formatted(missing)); + + Validate.isTrue(Objects.equals(configValue(PARTICIPANT_ID), configValue(ECLIPSE_EDC_PARTICIPANT_ID)), + "Config Properties %s and %s have different values. Please set %s instead of %s." + .formatted(PARTICIPANT_ID, ECLIPSE_EDC_PARTICIPANT_ID, PARTICIPANT_ID, ECLIPSE_EDC_PARTICIPANT_ID)); + + if (StringUtils.isNotBlank(configValue(NAME_KEBAB_CASE))) { + monitor.warning("Config Property %s is deprecated in favor of %s. Please configure that instead." + .formatted(NAME_KEBAB_CASE, PARTICIPANT_ID)); + } + } + + String configValue(String configKey) { + return config.getString(configKey, ""); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java new file mode 100644 index 000000000..21f46a5f3 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.policy; + + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.util.Comparator; +import java.util.List; + + +@RequiredArgsConstructor +public class PolicyDefinitionApiService { + + private final PolicyDefinitionService policyDefinitionService; + private final PolicyMapper policyMapper; + + public List getPolicyDefinitions() { + var policyDefinitions = getAllPolicyDefinitions(); + return policyDefinitions.stream() + .sorted(Comparator.comparing(PolicyDefinition::getCreatedAt).reversed()) + .map(this::buildPolicyDefinitionDto) + .toList(); + } + + @NotNull + public IdResponseDto createPolicyDefinition(PolicyDefinitionCreateRequest request) { + var policyDefinition = buildPolicyDefinition(request); + policyDefinition = policyDefinitionService.create(policyDefinition).orElseThrow(ServiceException::new); + return new IdResponseDto(policyDefinition.getId()); + } + + @NotNull + public IdResponseDto deletePolicyDefinition(String policyDefinitionId) { + var response = policyDefinitionService.deleteById(policyDefinitionId).orElseThrow(ServiceException::new); + return new IdResponseDto(response.getId()); + } + + private List getAllPolicyDefinitions() { + return policyDefinitionService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } + public PolicyDefinitionDto buildPolicyDefinitionDto(PolicyDefinition policyDefinition) { + var policy = policyMapper.buildUiPolicy(policyDefinition.getPolicy()); + return PolicyDefinitionDto.builder() + .policyDefinitionId(policyDefinition.getId()) + .policy(policy) + .build(); + } + + public PolicyDefinition buildPolicyDefinition(PolicyDefinitionCreateRequest policyDefinitionDto) { + var policy = policyMapper.buildPolicy(policyDefinitionDto.getPolicy()); + return PolicyDefinition.Builder.newInstance() + .id(policyDefinitionDto.getPolicyDefinitionId()) + .policy(policy) + .build(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java new file mode 100644 index 000000000..a72e1ddad --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementDirection; +import de.sovity.edc.ext.wrapper.api.ui.model.TransferHistoryEntry; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.entity.Entity; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BinaryOperator; +import java.util.function.Function; + +import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcMillisToOffsetDateTime; +import static java.util.stream.Collectors.toMap; + +@RequiredArgsConstructor +public class TransferHistoryPageApiService { + + private final AssetService assetService; + private final ContractAgreementService contractAgreementService; + private final ContractNegotiationStore contractNegotiationStore; + private final TransferProcessService transferProcessService; + private final TransferProcessStateService transferProcessStateService; + + /** + * Fetches all Transfer History entries as {@link TransferHistoryEntry}s. + * + * @return {@link TransferHistoryEntry}s + */ + @NotNull + public List getTransferHistoryEntries() { + + var negotiationsById = getAllContractNegotiations().stream() + .filter(negotiation -> negotiation != null) + .filter(negotiation -> negotiation.getContractAgreement() != null) + .collect(toMap( + it -> it.getContractAgreement().getId(), + Function.identity(), + BinaryOperator.maxBy(Comparator.comparing(Entity::getCreatedAt)) + )); + + var agreementsById = getAllContractAgreements().stream().collect(toMap( + ContractAgreement::getId, Function.identity() + )); + + var assetsById = getAllAssets().stream() + .collect(toMap(Asset::getId, Function.identity())); + + var transferProcesses = getAllTransferProcesses(); + + return transferProcesses.stream().map(process -> { + var agreement = Optional.ofNullable(agreementsById.get(process.getDataRequest().getContractId())); + var negotiation = Optional.ofNullable(negotiationsById.get(process.getDataRequest().getContractId())); + var asset = assetLookup(assetsById, process); + var direction = negotiation.map(ContractNegotiation::getType).map(ContractAgreementDirection::fromType); + var transferHistoryEntry = new TransferHistoryEntry(); + transferHistoryEntry.setAssetId(asset.getId()); + + if (direction.isPresent()) { + if (direction.get() == ContractAgreementDirection.CONSUMING) { + transferHistoryEntry.setAssetName(asset.getId()); + } else { + transferHistoryEntry.setAssetName( + StringUtils.isBlank((String) asset.getProperties().get(Prop.Dcterms.TITLE)) + ? asset.getId() + : asset.getProperties().get(Prop.Dcterms.TITLE).toString() + ); + } + } + + agreement.ifPresent(it -> transferHistoryEntry.setContractAgreementId(it.getId())); + negotiation.ifPresent( it -> { + transferHistoryEntry.setCounterPartyConnectorEndpoint(it.getCounterPartyAddress()); + transferHistoryEntry.setCounterPartyParticipantId(it.getCounterPartyId()); + transferHistoryEntry.setCreatedDate(utcMillisToOffsetDateTime(it.getCreatedAt())); + }); + direction.ifPresent(transferHistoryEntry::setDirection); + + transferHistoryEntry.setErrorMessage(process.getErrorDetail()); + transferHistoryEntry.setLastUpdatedDate(utcMillisToOffsetDateTime(process.getUpdatedAt())); + transferHistoryEntry.setState(transferProcessStateService.buildTransferProcessState(process.getState())); + transferHistoryEntry.setTransferProcessId(process.getId()); + return transferHistoryEntry; + }).sorted(Comparator.comparing(TransferHistoryEntry::getLastUpdatedDate).reversed()).toList(); + } + + private Asset assetLookup(Map assetsById, TransferProcess process) { + var assetId = process.getDataRequest().getAssetId(); + var asset = assetsById.get(assetId); + if (asset == null) { + return Asset.Builder.newInstance().id(assetId).build(); + } + return asset; + } + + @NotNull + private List getAllContractNegotiations() { + return contractNegotiationStore.queryNegotiations(QuerySpec.max()).toList(); + } + + @NotNull + private List getAllContractAgreements() { + return contractAgreementService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } + + @NotNull + private List getAllTransferProcesses() { + return transferProcessService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } + + @NotNull + private List getAllAssets() { + return assetService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java new file mode 100644 index 000000000..a00d3552c --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractNegotiationUtils; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor +public class TransferHistoryPageAssetFetcherService { + private final AssetService assetService; + private final TransferProcessService transferProcessService; + private final AssetMapper assetMapper; + private final ContractNegotiationStore contractNegotiationStore; + private final ContractNegotiationUtils contractNegotiationUtils; + + + public UiAsset getAssetForTransferHistoryPage(String transferProcessId) { + + var transferProcessById = transferProcessService.findById(transferProcessId); + if (transferProcessById == null) { + throw new EdcException("Could not find transfer process with ID %s.".formatted(transferProcessId)); + } + return getAssetFromTransferProcess(transferProcessById); + } + + @NotNull + private UiAsset getAssetFromTransferProcess(TransferProcess process) { + var asset = getTransferProcessAsset(process); + var negotiation = contractNegotiationUtils.findByContractAgreementIdOrThrow(process.getContractId()); + + // Additional Asset Metadata required for UI + return buildUiAsset(asset, negotiation); + } + + private Asset getTransferProcessAsset(TransferProcess process) { + var assetId = process.getDataRequest().getAssetId(); + var asset = assetService.findById(process.getDataRequest().getAssetId()); + if (asset == null) { + asset = Asset.Builder.newInstance().id(assetId).build(); + } + return asset; + } + + private UiAsset buildUiAsset(Asset asset, ContractNegotiation negotiation) { + var connectorEndpoint = contractNegotiationUtils.getProviderConnectorEndpoint(negotiation); + var participantId = contractNegotiationUtils.getProviderParticipantId(negotiation); + return assetMapper.buildUiAsset(asset, connectorEndpoint, participantId); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateService.java new file mode 100644 index 000000000..a3dfac07c --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateService.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState; +import de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessState; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.jetbrains.annotations.NotNull; + + +@RequiredArgsConstructor +public class TransferProcessStateService { + + /** + * Interpret {@link TransferProcess#getState()} for use in our UI. + * + * @param code {@link TransferProcess#getState()}, see {@link TransferProcessStates#code()} + * @return if running + */ + @NotNull + public TransferProcessState buildTransferProcessState(int code) { + var transferProcessState = new TransferProcessState(); + transferProcessState.setCode(code); + transferProcessState.setName(getName(code)); + transferProcessState.setSimplifiedState(getSimplifiedState(code)); + return transferProcessState; + } + + /** + * Which Transfer Process do we want to show as 'running' in our UI? + * + * @param code {@link TransferProcess#getState()}, see {@link TransferProcessStates#code()} + * @return if running + */ + public boolean isRunning(int code) { + // After this there are still states about de-provisioning of resources, + // but we don't really care much about them + return !isError(code) && code < TransferProcessStates.COMPLETED.code(); + } + + /** + * Which Transfer Process do we want to show as 'error' in our UI? + * + * @param code {@link TransferProcess#getState()}, see {@link TransferProcessStates#code()} + * @return if running + */ + public boolean isError(int code) { + return TransferProcessStates.TERMINATING.code() == code || TransferProcessStates.TERMINATED.code() == code; + } + + @NotNull + private String getName(int code) { + TransferProcessStates state = TransferProcessStates.from(code); + if (state != null) { + return state.name(); + } + + return "CUSTOM"; + } + + @NotNull + public TransferProcessSimplifiedState getSimplifiedState(int code) { + if (isError(code)) { + return TransferProcessSimplifiedState.ERROR; + } + if (isRunning(code)) { + return TransferProcessSimplifiedState.RUNNING; + } + return TransferProcessSimplifiedState.OK; + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java new file mode 100644 index 000000000..8418ea75f --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase; + +import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; +import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; +import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; +import lombok.RequiredArgsConstructor; + +import java.util.List; + + +/** + * Provides the endpoints for use-case specific requests. + */ +@RequiredArgsConstructor +public class UseCaseResourceImpl implements UseCaseResource { + private final KpiApiService kpiApiService; + private final SupportedPolicyApiService supportedPolicyApiService; + + @Override + public KpiResult getKpis() { + return kpiApiService.getKpis(); + } + + @Override + public List getSupportedFunctions() { + return supportedPolicyApiService.getSupportedFunctions(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/KpiApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/KpiApiService.java new file mode 100644 index 000000000..65bb5a272 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/KpiApiService.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.services; + +import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState; +import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; +import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; +import de.sovity.edc.ext.wrapper.api.usecase.model.TransferProcessStatesDto; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.connector.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.counting; +import static java.util.stream.Collectors.groupingBy; + +@RequiredArgsConstructor +public class KpiApiService { + private final AssetIndex assetIndex; + private final PolicyDefinitionStore policyDefinitionStore; + private final ContractDefinitionStore contractDefinitionStore; + private final TransferProcessStore transferProcessStore; + private final ContractAgreementService contractAgreementService; + private final TransferProcessStateService transferProcessStateService; + + public KpiResult getKpis() { + var assetsCount = getAssetsCount(); + var policiesCount = getPoliciesCount(); + var contractDefinitionsCount = getContractDefinitionsCount(); + var contractAgreements = getContractAgreementsCount(); + var transferProcessDto = getTransferProcessesDto(); + + return new KpiResult( + assetsCount, + policiesCount, + contractDefinitionsCount, + contractAgreements, + transferProcessDto + ); + } + + private int getContractAgreementsCount() { + return contractAgreementService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList().size(); + } + + private TransferProcessStatesDto getTransferProcessesDto() { + var transferProcesses = transferProcessStore.findAll(QuerySpec.max()).toList(); + return new TransferProcessStatesDto(getIncoming(transferProcesses), getOutgoing(transferProcesses)); + } + + private Map getIncoming(List transferProcesses) { + return transferProcesses.stream() + .filter(it -> it.getType() == TransferProcess.Type.CONSUMER) + .collect(groupingBy(this::getTransferProcessStates, counting())); + } + + private Map getOutgoing(List transferProcesses) { + return transferProcesses.stream() + .filter(it -> it.getType() == TransferProcess.Type.PROVIDER) + .collect(groupingBy(this::getTransferProcessStates, counting())); + } + + private TransferProcessSimplifiedState getTransferProcessStates(TransferProcess transferProcess) { + return transferProcessStateService.getSimplifiedState(transferProcess.getState()); + } + + private int getContractDefinitionsCount() { + var contractDefinitions = contractDefinitionStore.findAll(QuerySpec.max()).toList(); + return contractDefinitions.size(); + } + + private int getPoliciesCount() { + var policies = policyDefinitionStore.findAll(QuerySpec.max()).toList(); + return policies.size(); + } + + private int getAssetsCount() { + var assets = assetIndex.queryAssets(QuerySpec.max()).toList(); + return assets.size(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/SupportedPolicyApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/SupportedPolicyApiService.java new file mode 100644 index 000000000..2426fd1d9 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/services/SupportedPolicyApiService.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.services; + +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; + +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.wrapper.utils.FieldAccessUtils.accessField; + +@RequiredArgsConstructor +public class SupportedPolicyApiService { + private final PolicyEngine policyEngine; + + public List getSupportedFunctions() { + Map> constraintFunctions = accessField(policyEngine, "constraintFunctions"); + return constraintFunctions.values().stream().flatMap(List::stream) + .map(it -> (String) accessField(it, "key")) + .distinct() + .sorted() + .toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/EdcDateUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/EdcDateUtils.java new file mode 100644 index 000000000..28b1cfe7a --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/EdcDateUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.utils; + +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class EdcDateUtils { + + /** + * Build {@link OffsetDateTime} from UTC milliseconds since epoch. + *

      + * The EDC framework only uses longs to represent dates. + *

      + * We want to use real date types in our code. + * + * @param utcMillis milliseconds since epoch in UTC + * @return {@link OffsetDateTime} or null + */ + public static OffsetDateTime utcMillisToOffsetDateTime(Long utcMillis) { + if (utcMillis == null) { + return null; + } + return OffsetDateTime.ofInstant(java.time.Instant.ofEpochMilli(utcMillis), java.time.ZoneOffset.UTC); + } + + /** + * Build {@link OffsetDateTime} from UTC seconds since epoch. + *

      + * The EDC framework only uses longs to represent dates. + *

      + * We want to use real date types in our code. + * + * @param utcSeconds seconds since epoch in UTC + * @return {@link OffsetDateTime} or null + */ + public static OffsetDateTime utcSecondsToOffsetDateTime(Long utcSeconds) { + if (utcSeconds == null) { + return null; + } + return OffsetDateTime.ofInstant(java.time.Instant.ofEpochSecond(utcSeconds), java.time.ZoneOffset.UTC); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/FieldAccessUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/FieldAccessUtils.java new file mode 100644 index 000000000..98f5a23cf --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/FieldAccessUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + +import java.util.List; + +/** + * Where there's a will, there's a way. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FieldAccessUtils { + + /** + * Access an object's private field's values recursively. + * + * @param o object + * @param fieldNamePath field names + * @param return type + * @return field value + */ + @SneakyThrows + @SuppressWarnings("unchecked") + public static T accessField(Object o, List fieldNamePath) { + Object result = o; + for (String fieldName : fieldNamePath) { + result = accessField(result, fieldName); + } + return (T) result; + } + + /** + * Access an object's private field's value. + * + * @param o object + * @param fieldName field name + * @param return type + * @return field value + */ + @SneakyThrows + @SuppressWarnings("unchecked") + public static T accessField(Object o, String fieldName) { + var field = o.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(o); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/MapUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/MapUtils.java new file mode 100644 index 000000000..b7bc9d297 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/MapUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.utils; + +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toMap; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class MapUtils { + + public static Map mapValues(@NonNull Map map, @NonNull Function valueMapper) { + return map.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> valueMapper.apply(e.getValue()))); + } + + public static Map associateBy(Collection collection, Function keyExtractor) { + return collection.stream().collect(Collectors.toMap(keyExtractor, Function.identity(), (a, b) -> { + throw new IllegalStateException("Duplicate key %s.".formatted(keyExtractor.apply(a))); + })); + } + + public static Map reverse(@NonNull Map map) { + return map.entrySet().stream().collect(toMap(Map.Entry::getValue, Map.Entry::getKey)); + } +} diff --git a/extensions/wrapper/wrapper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/wrapper/wrapper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..e7f3d2c32 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.ext.wrapper.WrapperExtension diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java new file mode 100644 index 000000000..12054c816 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper; + +import de.sovity.edc.client.EdcClient; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.protocol.ProtocolWebhook; + +import java.util.HashMap; +import java.util.Map; + +public class TestUtils { + private static final int MANAGEMENT_PORT = 34002; + private static final int PROTOCOL_PORT = 34003; + private static final int WEB_PORT = 34001; + private static final String MANAGEMENT_PATH = "/api/management"; + private static final String PROTOCOL_PATH = "/api/dsp"; + public static final String MANAGEMENT_API_KEY = "123456"; + public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + MANAGEMENT_PORT + MANAGEMENT_PATH; + + + public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; + public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH; + + public static Map createConfiguration( + Map additionalConfigProperties + ) { + Map config = new HashMap<>(); + config.put("web.http.port", String.valueOf(WEB_PORT)); + config.put("web.http.path", "/api"); + config.put("web.http.management.port", String.valueOf(MANAGEMENT_PORT)); + config.put("web.http.management.path", MANAGEMENT_PATH); + config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); + config.put("web.http.protocol.path", PROTOCOL_PATH); + config.put("edc.api.auth.key", MANAGEMENT_API_KEY); + config.put("edc.dsp.callback.address", PROTOCOL_ENDPOINT); + config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); + + config.put("edc.participant.id", "my-edc-participant-id"); + config.put("my.edc.participant.id", "my-edc-participant-id"); + config.put("my.edc.title", "My Connector"); + config.put("my.edc.description", "My Connector Description"); + config.put("my.edc.curator.url", "https://connector.my-org"); + config.put("my.edc.curator.name", "My Org"); + config.put("my.edc.maintainer.url", "https://maintainer-org"); + config.put("my.edc.maintainer.name", "Maintainer Org"); + + config.put("edc.oauth.token.url", "https://token-url.daps"); + config.put("edc.oauth.provider.jwks.url", "https://jwks-url.daps"); + config.put("tx.ssi.miw.authority.id", "my-authority-id"); + config.put("tx.ssi.miw.url", "https://miw"); + config.put("tx.ssi.oauth.token.url", "https://token.miw"); + config.putAll(additionalConfigProperties); + return config; + } + + public static void setupExtension(EdcExtension extension) { + System.out.println("Hello World from TestUtils#setupExtension!"); + setupExtension(extension, Map.of()); + } + + public static void setupExtension(EdcExtension extension, Map configProperties) { + extension.registerServiceMock(ProtocolWebhook.class, () -> PROTOCOL_ENDPOINT); + extension.setConfiguration(createConfiguration(configProperties)); + } + + public static EdcClient edcClient() { + return EdcClient.builder() + .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) + .managementApiKey(TestUtils.MANAGEMENT_API_KEY) + .build(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java new file mode 100644 index 000000000..adc3bc332 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.wrapper.api.ui.pages.asset; + + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.UiAsset; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.SneakyThrows; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +public class AssetApiServiceTest { + + public static final String DATA_SINK = "http://my-data-sink/api/stuff"; + EdcClient client; + EdcPropertyUtils edcPropertyUtils; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + edcPropertyUtils = new EdcPropertyUtils(); + client = TestUtils.edcClient(); + } + + @Test + void assetPage(AssetService assetStore) { + // arrange + var properties = Map.of( + Asset.PROPERTY_ID, "asset-1", + Prop.Dcat.LANDING_PAGE, "https://data-source.my-org/docs" + ); + createAsset(assetStore, "2023-06-01", properties); + + // act + var result = client.uiApi().getAssetPage(); + + // assert + var assets = result.getAssets(); + assertThat(assets).hasSize(1); + var asset = assets.get(0); + assertThat(asset.getAssetId()).isEqualTo(properties.get(Asset.PROPERTY_ID)); + assertThat(asset.getLandingPageUrl()).isEqualTo(properties.get(Prop.Dcat.LANDING_PAGE)); + } + + @Test + void assetPageSorting(AssetService assetService) { + // arrange + createAsset(assetService, "2023-06-01", Map.of(Asset.PROPERTY_ID, "asset-1")); + createAsset(assetService, "2023-06-03", Map.of(Asset.PROPERTY_ID, "asset-3")); + createAsset(assetService, "2023-06-02", Map.of(Asset.PROPERTY_ID, "asset-2")); + + // act + var result = client.uiApi().getAssetPage(); + + // assert + assertThat(result.getAssets()) + .extracting(UiAsset::getAssetId) + .containsExactly("asset-3", "asset-2", "asset-1"); + } + + @Test + void testAssetCreation(AssetService assetService) { + // arrange + var dataAddressProperties = Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.BASE_URL, DATA_SINK, + Prop.Edc.PROXY_METHOD, "true", + Prop.Edc.PROXY_PATH, "true", + Prop.Edc.PROXY_QUERY_PARAMS, "true", + Prop.Edc.PROXY_BODY, "true", + + // tests that a property without a context URL will survive the JSON-LD mapping + "oauth2:tokenUrl", "https://token-url" + ); + var uiAssetRequest = UiAssetCreateRequest.builder() + .id("asset-1") + .title("AssetTitle") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .sovereignLegalName("my sovereign") + .geoLocation("40.0, 40.0") + .nutsLocations(Arrays.asList("DE", "DE929")) + .dataSampleUrls(Arrays.asList("https://sample-a", "https://sample-b")) + .referenceFileUrls(Arrays.asList("https://reference-a", "https://reference-b")) + .referenceFilesDescription("RF Description") + .conditionsForUse("Conditions for use") + .dataUpdateFrequency("every month") + .temporalCoverageFrom(LocalDate.of(2020, 1, 1)) + .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataAddressProperties(dataAddressProperties) + .customJsonAsString("{\"test\":\"value\"}") + .customJsonLdAsString(""" + { + "https://string": "value", + "https://number": 3.14, + "https://array": [1,2,3], + "https://object": { "https://key": "value" }, + "https://booleans/are/not/supported/by/Eclipse/EDC": true, + "https://null/will/be/eliminated": null + } + """) + .privateCustomJsonAsString("{\"private test\":\"private value\"}") + .privateCustomJsonLdAsString(""" + { + "https://private/string": "value", + "https://private/number": 3.14, + "https://private/array": [1,2,3], + "https://private/object": { "https://key": "value" }, + "https://private/booleans/are/not/supported/by/Eclipse/EDC": true, + "https://private/null/will/be/eliminated": null + } + """) + .build(); + + // act + var response = client.uiApi().createAsset(uiAssetRequest); + + // assert + assertThat(response.getId()).isEqualTo("asset-1"); + + var assets = client.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + var asset = assets.get(0); + assertThat(asset.getAssetId()).isEqualTo("asset-1"); + assertThat(asset.getTitle()).isEqualTo("AssetTitle"); + assertThat(asset.getDescription()).isEqualTo("AssetDescription"); + assertThat(asset.getVersion()).isEqualTo("1.0.0"); + assertThat(asset.getLanguage()).isEqualTo("en"); + assertThat(asset.getMediaType()).isEqualTo("application/json"); + assertThat(asset.getDataCategory()).isEqualTo("dataCategory"); + assertThat(asset.getDataSubcategory()).isEqualTo("dataSubcategory"); + assertThat(asset.getDataModel()).isEqualTo("dataModel"); + assertThat(asset.getGeoReferenceMethod()).isEqualTo("geoReferenceMethod"); + assertThat(asset.getTransportMode()).isEqualTo("transportMode"); + assertThat(asset.getSovereignLegalName()).isEqualTo("my sovereign"); + assertThat(asset.getGeoLocation()).isEqualTo("40.0, 40.0"); + assertThat(asset.getNutsLocations()).isEqualTo(Arrays.asList("DE", "DE929")); + assertThat(asset.getDataSampleUrls()).isEqualTo(Arrays.asList("https://sample-a", "https://sample-b")); + assertThat(asset.getReferenceFileUrls()).isEqualTo(Arrays.asList("https://reference-a", "https://reference-b")); + assertThat(asset.getReferenceFilesDescription()).isEqualTo("RF Description"); + assertThat(asset.getConditionsForUse()).isEqualTo("Conditions for use"); + assertThat(asset.getDataUpdateFrequency()).isEqualTo("every month"); + assertThat(asset.getTemporalCoverageFrom()).isEqualTo(LocalDate.of(2020, 1, 1)); + assertThat(asset.getTemporalCoverageToInclusive()).isEqualTo(LocalDate.of(2020, 1, 8)); + assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url"); + assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); + assertThat(asset.getCreatorOrganizationName()).isEqualTo("My Org"); + assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage"); + assertThat(asset.getHttpDatasourceHintsProxyMethod()).isTrue(); + assertThat(asset.getHttpDatasourceHintsProxyPath()).isTrue(); + assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); + assertThat(asset.getHttpDatasourceHintsProxyBody()).isTrue(); + assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" + { "test": "value" } + """); + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { + "https://string": "value", + "https://number": 3.14, + "https://array": [1.0, 2.0, 3.0], + "https://object": { "https://key": "value" } + } + """); + assertThatJson(asset.getPrivateCustomJsonAsString()).isEqualTo(""" + { "private test": "private value" } + """); + assertThatJson(asset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { + "https://private/string": "value", + "https://private/number": 3.14, + "https://private/array": [1.0, 2.0, 3.0], + "https://private/object": { "https://key": "value" } + } + """); + + var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); + assertThat(assetWithDataAddress.getDataAddress().getProperties()).isEqualTo(dataAddressProperties); + } + + @Test + void testEditAssetMetadata(AssetService assetService) { + // arrange + var dataAddress = Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.BASE_URL, DATA_SINK, + Prop.Edc.PROXY_METHOD, "true", + Prop.Edc.PROXY_PATH, "true", + Prop.Edc.PROXY_QUERY_PARAMS, "true", + Prop.Edc.PROXY_BODY, "true", + "oauth2:tokenUrl", "https://token-url" + ); + var createRequest = UiAssetCreateRequest.builder() + .id("asset-1") + .title("AssetTitle") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .sovereignLegalName("my sovereign") + .geoLocation("40.0, 40.0") + .nutsLocations(Arrays.asList("DE", "DE929")) + .dataSampleUrls(Arrays.asList("https://sample-a", "https://sample-b")) + .referenceFileUrls(Arrays.asList("https://reference-a", "https://reference-b")) + .referenceFilesDescription("RF Description") + .conditionsForUse("Conditions for use") + .dataUpdateFrequency("every month") + .temporalCoverageFrom(LocalDate.of(2020, 1, 1)) + .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataAddressProperties(dataAddress) + .customJsonAsString(""" + { "test": "value" } + """) + .customJsonLdAsString(""" + { + "https://to-change": "value1", + "https://for-deletion": "value2" + } + """) + .build(); + + client.uiApi().createAsset(createRequest); + + var editRequest = UiAssetEditMetadataRequest.builder() + .title("AssetTitle 2") + .description("AssetDescription 2") + .licenseUrl("https://license-url/2") + .version("2.0.0") + .language("de") + .mediaType("application/json+utf8") + .dataCategory("dataCategory2") + .dataSubcategory("dataSubcategory2") + .dataModel("dataModel2") + .geoReferenceMethod("geoReferenceMethod2") + .sovereignLegalName("my sovereign2") + .geoLocation("50.0, 50.0") + .nutsLocations(Arrays.asList("NL", "NL929")) + .dataSampleUrls(Arrays.asList("https://sample-a2", "https://sample-b2")) + .referenceFileUrls(Arrays.asList("https://reference-a2", "https://reference-b2")) + .referenceFilesDescription("RF Description2") + .conditionsForUse("Conditions for use2") + .dataUpdateFrequency("every week") + .temporalCoverageFrom(LocalDate.of(2021, 1, 1)) + .temporalCoverageToInclusive(LocalDate.of(2021, 1, 8)) + .transportMode("transportMode2") + .keywords(List.of("keyword3")) + .publisherHomepage("publisherHomepage2") + .customJsonAsString(""" + { "edited": "new value" } + """) + .customJsonLdAsString(""" + { + "https://to-change": "new value LD", + "https://for-deletion": null + } + """) + .build(); + + // act + var response = client.uiApi().editAssetMetadata("asset-1", editRequest); + + // assert + assertThat(response.getId()).isEqualTo("asset-1"); + + var assets = client.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + var asset = assets.get(0); + assertThat(asset.getAssetId()).isEqualTo("asset-1"); + assertThat(asset.getTitle()).isEqualTo("AssetTitle 2"); + assertThat(asset.getDescription()).isEqualTo("AssetDescription 2"); + assertThat(asset.getVersion()).isEqualTo("2.0.0"); + assertThat(asset.getLanguage()).isEqualTo("de"); + assertThat(asset.getMediaType()).isEqualTo("application/json+utf8"); + assertThat(asset.getDataCategory()).isEqualTo("dataCategory2"); + assertThat(asset.getDataSubcategory()).isEqualTo("dataSubcategory2"); + assertThat(asset.getDataModel()).isEqualTo("dataModel2"); + assertThat(asset.getGeoReferenceMethod()).isEqualTo("geoReferenceMethod2"); + assertThat(asset.getTransportMode()).isEqualTo("transportMode2"); + assertThat(asset.getSovereignLegalName()).isEqualTo("my sovereign2"); + assertThat(asset.getGeoLocation()).isEqualTo("50.0, 50.0"); + assertThat(asset.getNutsLocations()).isEqualTo(Arrays.asList("NL", "NL929")); + assertThat(asset.getDataSampleUrls()).isEqualTo(Arrays.asList("https://sample-a2", "https://sample-b2")); + assertThat(asset.getReferenceFileUrls()).isEqualTo(Arrays.asList("https://reference-a2", "https://reference-b2")); + assertThat(asset.getReferenceFilesDescription()).isEqualTo("RF Description2"); + assertThat(asset.getConditionsForUse()).isEqualTo("Conditions for use2"); + assertThat(asset.getDataUpdateFrequency()).isEqualTo("every week"); + assertThat(asset.getTemporalCoverageFrom()).isEqualTo(LocalDate.of(2021, 1, 1)); + assertThat(asset.getTemporalCoverageToInclusive()).isEqualTo(LocalDate.of(2021, 1, 8)); + assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url/2"); + assertThat(asset.getKeywords()).isEqualTo(List.of("keyword3")); + assertThat(asset.getCreatorOrganizationName()).isEqualTo("My Org"); + assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage2"); + assertThat(asset.getHttpDatasourceHintsProxyMethod()).isTrue(); + assertThat(asset.getHttpDatasourceHintsProxyPath()).isTrue(); + assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); + assertThat(asset.getHttpDatasourceHintsProxyBody()).isTrue(); + assertThat(asset.getCustomJsonAsString()).isEqualTo(""" + { "edited": "new value" } + """); + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { "https://to-change": "new value LD" } + """); + + var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); + assertThat(assetWithDataAddress.getDataAddress().getProperties()).isEqualTo(dataAddress); + } + + @Test + void testAssetCreation_noProxying() { + // arrange + var dataAddressProperties = Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.BASE_URL, DATA_SINK + ); + var uiAssetRequest = UiAssetCreateRequest.builder() + .id("asset-1") + .dataAddressProperties(dataAddressProperties) + .build(); + + // act + var response = client.uiApi().createAsset(uiAssetRequest); + + // assert + assertThat(response.getId()).isEqualTo("asset-1"); + var assets = client.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + var asset = assets.get(0); + assertThat(asset.getHttpDatasourceHintsProxyMethod()).isFalse(); + assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); + assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isFalse(); + assertThat(asset.getHttpDatasourceHintsProxyBody()).isFalse(); + } + + @Test + void testAssetCreation_differentDataAddressType() { + // arrange + var dataAddressProperties = Map.of( + Prop.Edc.TYPE, "Unknown" + ); + var uiAssetRequest = UiAssetCreateRequest.builder() + .id("asset-1") + .dataAddressProperties(dataAddressProperties) + .build(); + + // act + var response = client.uiApi().createAsset(uiAssetRequest); + + // assert + assertThat(response.getId()).isEqualTo("asset-1"); + var assets = client.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + var asset = assets.get(0); + assertThat(asset.getHttpDatasourceHintsProxyMethod()).isNull(); + assertThat(asset.getHttpDatasourceHintsProxyPath()).isNull(); + assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isNull(); + assertThat(asset.getHttpDatasourceHintsProxyBody()).isNull(); + } + + @Test + void testDeleteAsset(AssetService assetService) { + // arrange + createAsset(assetService, "2023-06-01", Map.of(Asset.PROPERTY_ID, "asset-1")); + assertThat(assetService.query(QuerySpec.max()).getContent()).isNotEmpty(); + + // act + var response = client.uiApi().deleteAsset("asset-1"); + + // assert + assertThat(response.getId()).isEqualTo("asset-1"); + assertThat(assetService.query(QuerySpec.max()).getContent()).isEmpty(); + } + + private void createAsset( + AssetService assetService, + String date, + Map properties + ) { + DataAddress dataAddress = DataAddress.Builder.newInstance() + .type("HttpData") + .property(Prop.Edc.BASE_URL, DATA_SINK) + .build(); + + var asset = Asset.Builder.newInstance() + .id(properties.get(Asset.PROPERTY_ID)) + .properties(edcPropertyUtils.toMapOfObject(properties)) + .dataAddress(dataAddress) + .createdAt(dateFormatterToLong(date)) + .build(); + assetService.create(asset); + } + + @SneakyThrows + private static long dateFormatterToLong(String date) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + return formatter.parse(date).getTime(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidatorTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidatorTest.java new file mode 100644 index 000000000..abe539b0c --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetIdValidatorTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.asset; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AssetIdValidatorTest { + AssetIdValidator assetIdValidator; + + @BeforeEach + void setup() { + assetIdValidator = new AssetIdValidator(); + } + + @Test + void testOk() { + var assetId = "test-1.0"; + assertThat(assetIdValidator.isValid(assetId)).isTrue(); + assetIdValidator.assertValid(assetId); + } + + @Test + void testColon() { + var assetId = "test:1.0"; + assertThat(assetIdValidator.isValid(assetId)).isFalse(); + assertThatThrownBy(() -> assetIdValidator.assertValid(assetId)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testWhitespace() { + var assetId = "test asset"; + assertThat(assetIdValidator.isValid(assetId)).isFalse(); + assertThatThrownBy(() -> assetIdValidator.assertValid(assetId)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java new file mode 100644 index 000000000..9030a3ea2 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java @@ -0,0 +1,110 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.catalog; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + + +@ApiTest +@ExtendWith(EdcExtension.class) +public class CatalogApiTest { + private EdcClient client; + private final String dataOfferId = "my-data-offer-2023-11"; + + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + /** + * There used to be issues with the Prop.DISTRIBUTION field being occupied by core EDC. + * This test verifies that the field can be used by us. + */ + @DisabledOnGithub + @Test + void test_Distribution_Key() { + // arrange + createAsset(); + createPolicy(); + createContractDefinition(); + // act + var catalogPageDataOffers = client.uiApi().getCatalogPageDataOffers(TestUtils.PROTOCOL_ENDPOINT); + + // assert + assertThat(catalogPageDataOffers.size()).isEqualTo(1); + assertThat(catalogPageDataOffers.get(0).getAsset().getTitle()).isEqualTo("My Data Offer"); + assertThat(catalogPageDataOffers.get(0).getAsset().getMediaType()).isEqualTo("Media Type"); + } + + private void createAsset() { + var asset = UiAssetCreateRequest.builder() + .id(dataOfferId) + .title("My Data Offer") + .description("Example Data Offer.") + .version("2023-11") + .language("EN") + .publisherHomepage("https://my-department.my-org.com/my-data-offer") + .licenseUrl("https://my-department.my-org.com/my-data-offer#license") + .mediaType("Media Type") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, "https://a" + )) + .build(); + + client.uiApi().createAsset(asset); + } + + private void createPolicy() { + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(dataOfferId) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build(); + + client.uiApi().createPolicyDefinition(policyDefinition); + } + + private void createContractDefinition() { + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); + + client.uiApi().createContractDefinition(contractDefinition); + } + +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java new file mode 100644 index 000000000..b8defda15 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreement; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractAgreementDirection; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.connector.transfer.spi.types.DataRequest; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.LiteralExpression; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.net.URI; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class ContractAgreementPageTest { + + private static final int CONTRACT_DEFINITION_ID = 1; + private static final String ASSET_ID = UUID.randomUUID().toString(); + + EdcClient client; + LocalDate today = LocalDate.parse("2019-04-01"); + ZonedDateTime todayAsZonedDateTime = today.atStartOfDay(ZoneId.systemDefault()); + long todayEpochMillis = todayAsZonedDateTime.toInstant().toEpochMilli(); + long todayEpochSeconds = todayAsZonedDateTime.toInstant().getEpochSecond(); + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void testContractAgreementPage( + ContractNegotiationStore contractNegotiationStore, + TransferProcessStore transferProcessStore, + AssetIndex assetIndex + ) { + + // arrange + assetIndex.create(asset(ASSET_ID)).orElseThrow(storeFailure -> new RuntimeException("Failed to create asset")); + contractNegotiationStore.save(contractDefinition(CONTRACT_DEFINITION_ID)); + transferProcessStore.save(transferProcess(1, 1, TransferProcessStates.COMPLETED.code())); + + // act + var actual = client.uiApi().getContractAgreementPage().getContractAgreements(); + assertThat(actual).hasSize(1); + + // assert + var agreement = actual.get(0); + assertThat(agreement.getContractAgreementId()).isEqualTo("my-contract-agreement-1"); + assertThat(agreement.getContractNegotiationId()).isEqualTo("my-contract-negotiation-1"); + assertThat(agreement.getDirection()).isEqualTo(ContractAgreementDirection.PROVIDING); + assertThat(agreement.getCounterPartyAddress()).isEqualTo("http://other-connector"); + assertThat(agreement.getCounterPartyId()).isEqualTo("urn:connector:other-connector"); + assertThat(agreement.getContractSigningDate()).isEqualTo(todayPlusDays(0)); + assertThat(agreement.getAsset().getAssetId()).isEqualTo(ASSET_ID); + assertThat(agreement.getAsset().getLandingPageUrl()).isEqualTo("X"); + assertThat(agreement.getTransferProcesses()).hasSize(1); + + var transfer = agreement.getTransferProcesses().get(0); + assertThat(transfer.getTransferProcessId()).isEqualTo("my-transfer-1-1"); + assertThat(transfer.getLastUpdatedDate()).isNotNull(); + assertThat(transfer.getState().getName()).isEqualTo("COMPLETED"); + assertThat(transfer.getState().getCode()).isEqualTo(800); + assertThat(transfer.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + assertThat(transfer.getErrorMessage()).isEqualTo("my-error-message-1"); + + var constraint = agreement.getContractPolicy().getConstraints().get(0); + assertThat(constraint.getLeft()).isEqualTo("ALWAYS_TRUE"); + assertThat(constraint.getOperator()).isEqualTo(OperatorDto.EQ); + assertThat(constraint.getRight().getValue()).isEqualTo("true"); + } + + private DataAddress dataAddress() { + return DataAddress.Builder.newInstance() + .type("HttpData") + .properties(Map.of("baseUrl", "http://some-url")) + .build(); + } + + private TransferProcess transferProcess(int contract, int transfer, int code) { + var dataRequest = DataRequest.Builder.newInstance() + .contractId("my-contract-agreement-" + contract) + .assetId("my-asset-" + contract) + .processId("my-transfer-" + contract + "-" + transfer) + .id("my-data-request-" + contract + "-" + transfer) + .processId("my-transfer-" + contract + "-" + transfer) + .connectorAddress("http://other-connector") + .connectorId("urn:connector:other-connector") + .dataDestination(DataAddress.Builder.newInstance().type("HttpData").build()) + .build(); + return TransferProcess.Builder.newInstance() + .id("my-transfer-" + contract + "-" + transfer) + .state(code) + .type(TransferProcess.Type.PROVIDER) + .dataRequest(dataRequest) + .contentDataAddress(DataAddress.Builder.newInstance().type("HttpData").build()) + .errorDetail("my-error-message-" + transfer) + .build(); + } + + private ContractNegotiation contractDefinition(int contract) { + var agreement = ContractAgreement.Builder.newInstance() + .id("my-contract-agreement-" + contract) + .assetId(ASSET_ID) + .contractSigningDate(todayEpochSeconds) + .policy(alwaysTrue()) + .providerId(URI.create("http://other-connector").toString()) + .consumerId(URI.create("http://my-connector").toString()) + .build(); + + // Contract Negotiations can contain multiple Contract Offers (?) + // Test this + var irrelevantOffer = ContractOffer.Builder.newInstance() + .id("my-contract-offer-" + contract + "-irrelevant") + .assetId(asset(contract + "-irrelevant").getId()) + .policy(alwaysTrue()) + .build(); + + var offer = ContractOffer.Builder.newInstance() + .id("my-contract-offer-" + contract) + .assetId(ASSET_ID) + .policy(alwaysTrue()) + .build(); + + return ContractNegotiation.Builder.newInstance() + .correlationId("my-correlation-" + contract) + .contractAgreement(agreement) + .id("my-contract-negotiation-" + contract) + .counterPartyAddress("http://other-connector") + .counterPartyId("urn:connector:other-connector") + .protocol("ids") + .type(ContractNegotiation.Type.PROVIDER) + .contractOffers(List.of(irrelevantOffer, offer)) + .build(); + } + + private Asset asset(String assetId) { + return Asset.Builder.newInstance() + .id(assetId) + .property(Prop.Dcat.LANDING_PAGE, "X") + .createdAt(todayEpochMillis) + .dataAddress(dataAddress()) + .build(); + } + + + private Policy alwaysTrue() { + var alwaysTrueConstraint = AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression("ALWAYS_TRUE")) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression("true")) + .build(); + var alwaysTruePermission = Permission.Builder.newInstance() + .action(Action.Builder.newInstance().type("USE").build()) + .constraint(alwaysTrueConstraint) + .build(); + return Policy.Builder.newInstance() + .permission(alwaysTruePermission) + .build(); + } + + private String todayPlusDays(int i) { + return todayAsZonedDateTime.plusDays(i).toInstant().toString(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java new file mode 100644 index 000000000..284b375ce --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreement; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class ContractAgreementTransferApiServiceTest { + + private static final String DATA_SINK = "http://my-data-sink/api/stuff"; + private static final String COUNTER_PARTY_ADDRESS = + "http://some-other-connector/api/v1/ids/data"; + + EdcClient client; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void startTransferProcessForAgreementId( + ContractNegotiationStore store, + TransferProcessStore transferProcessStore + ) { + // arrange + var contractId = UUID.randomUUID().toString(); + createContractNegotiation(store, COUNTER_PARTY_ADDRESS, contractId); + + var request = new InitiateTransferRequest( + contractId, + Map.of( + "type", "HttpData", + "baseUrl", DATA_SINK + ), + Map.of("privateProperty", "privateValue") + ); + + // act + var result = client.uiApi().initiateTransfer(request); + + // then + var transferProcess = transferProcessStore.findById(result.getId()); + assertThat(transferProcess).isNotNull(); + assertThat(transferProcess.getPrivateProperties()).containsAllEntriesOf(Map.of( + "privateProperty", "privateValue" + )); + + var dataRequest = transferProcess.getDataRequest(); + assertThat(dataRequest.getContractId()).isEqualTo(contractId); + assertThat(dataRequest.getConnectorAddress()).isEqualTo(COUNTER_PARTY_ADDRESS); + assertThat(dataRequest.getDataDestination().getProperties()).containsAllEntriesOf(Map.of( + "https://w3id.org/edc/v0.0.1/ns/type", "HttpData", + "baseUrl", DATA_SINK + )); + } + + @Test + void startCustomTransferProcessForAgreementId( + ContractNegotiationStore store, + TransferProcessStore transferProcessStore + ) { + // arrange + var contractId = UUID.randomUUID().toString(); + createContractNegotiation(store, COUNTER_PARTY_ADDRESS, contractId); + + var customRequestJson = Json.createObjectBuilder() + .add(Prop.Edc.DATA_DESTINATION, Json.createObjectBuilder() + .add(Prop.Edc.TYPE, "HttpData") + .add(Prop.Edc.BASE_URL, DATA_SINK)) + .add(Prop.Edc.PRIVATE_PROPERTIES, Json.createObjectBuilder() + .add(Prop.Edc.RECEIVER_HTTP_ENDPOINT, "http://my-pull-backend") + .add("this-will-disappear", "because-its-not-an-url") + .add("http://unknown/custom-prop", "value")) + .build(); + var request = new InitiateCustomTransferRequest( + contractId, + JsonUtils.toJson(customRequestJson) + ); + + // act + var result = client.uiApi().initiateCustomTransfer(request); + + // then + var transferProcess = transferProcessStore.findById(result.getId()); + assertThat(transferProcess).isNotNull(); + assertThat(transferProcess.getPrivateProperties()).containsAllEntriesOf(Map.of( + Prop.Edc.RECEIVER_HTTP_ENDPOINT, "http://my-pull-backend", + "http://unknown/custom-prop", "value" + )); + + var dataRequest = transferProcess.getDataRequest(); + assertThat(dataRequest.getContractId()).isEqualTo(contractId); + assertThat(dataRequest.getConnectorAddress()).isEqualTo(COUNTER_PARTY_ADDRESS); + assertThat(dataRequest.getDataDestination().getProperties()).containsAllEntriesOf(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.BASE_URL, DATA_SINK + )); + } + + private ContractNegotiation createContractNegotiation( + ContractNegotiationStore store, + String counterPartyAddress, + String agreementId + ) { + var assetId = UUID.randomUUID().toString(); + var agreement = ContractAgreement.Builder.newInstance() + .id(agreementId) + .providerId(UUID.randomUUID().toString()) + .consumerId(UUID.randomUUID().toString()) + .assetId(assetId) + .policy(getPolicy()) + .build(); + + var negotiation = ContractNegotiation.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .counterPartyId(UUID.randomUUID().toString()) + .counterPartyAddress(counterPartyAddress) + .protocol(HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .contractAgreement(agreement) + .contractOffer(createContractOffer(assetId)) + .state(ContractNegotiationStates.FINALIZED.code()) + .build(); + + store.save(negotiation); + return negotiation; + } + + private Policy getPolicy() { + return Policy.Builder.newInstance().build(); + } + + private ContractOffer createContractOffer(String assetId) { + return ContractOffer.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .assetId(assetId) + .policy(getPolicy()) + .build(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java new file mode 100644 index 000000000..01a36294a --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; + +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; +import lombok.val; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TransferRequestBuilderTest { + @Test + void ensureThatThePreviousCustomPropertiesAreCopiedOverToTheDataSource() { + // arrange + var request = new InitiateTransferRequest( + "contract-id", + Map.of( + "type", "HttpData", + "baseUrl", "http://example.com/segment0" + ), + Map.of( + EDC_NAMESPACE + "pathSegments", "my-endpoint", + EDC_NAMESPACE + "method", "METHOD", + EDC_NAMESPACE + "queryParams", "queryParams", + EDC_NAMESPACE + "contentType", "mimetype", + EDC_NAMESPACE + "body", "[]" + ) + ); + + final var transformer = createTransferRequestBuilder(); + + val ROOT_KEY = "https://sovity.de/workaround/proxy/param/"; + + // act + val actual = transformer.buildCustomTransferRequest(request); + + // assert + val workaroundProperties = actual.getDataDestination().getProperties(); + assertThat(workaroundProperties).isNotEmpty(); + assertThat(workaroundProperties.get(ROOT_KEY + "pathSegments")).isEqualTo("my-endpoint"); + assertThat(workaroundProperties.get(ROOT_KEY + "method")).isEqualTo("METHOD"); + assertThat(workaroundProperties.get(ROOT_KEY + "queryParams")).isEqualTo("queryParams"); + assertThat(workaroundProperties.get(ROOT_KEY + "mediaType")).isEqualTo("mimetype"); + assertThat(workaroundProperties.get(ROOT_KEY + "body")).isEqualTo("[]"); + } + + @NotNull + private static TransferRequestBuilder createTransferRequestBuilder() { + val agreement = ContractAgreement.Builder.newInstance() + .id("contract-agreement-id") + .assetId("asset-id") + .providerId("provider-id") + .consumerId("consumer-id") + .policy(Policy.Builder.newInstance().build()) + .build(); + + val contractAgreementUtils = mock(ContractAgreementUtils.class); + when(contractAgreementUtils.findByIdOrThrow(any())).thenReturn(agreement); + + val contractNegotiationUtils = mock(ContractNegotiationUtils.class); + + val contractNegotiation = ContractNegotiation.Builder.newInstance() + .id("contract-negotiation-id") + .type(ContractNegotiation.Type.CONSUMER) + .counterPartyId("counter-party-id") + .counterPartyAddress("counter-party-address") + .protocol("protocol") + .contractAgreement(agreement) + .build(); + when(contractNegotiationUtils.findByContractAgreementIdOrThrow(any())).thenReturn(contractNegotiation); + + val transformer = new TransferRequestBuilder( + contractAgreementUtils, + contractNegotiationUtils, + new EdcPropertyUtils(), + mock(TypeTransformerRegistry.class), + new ParameterizationCompatibilityUtils() + ); + return transformer; + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java new file mode 100644 index 000000000..f2b8cc420 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java @@ -0,0 +1,184 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionEntry; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.ext.wrapper.TestUtils; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class ContractDefinitionPageApiServiceTest { + + // arrange + private EdcClient client; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void contractDefinitionPage(ContractDefinitionService contractDefinitionService) { + var criterion = new Criterion("exampleLeft1", "=", "abc"); + createContractDefinition(contractDefinitionService, "contractDefinition-id-1", "contractPolicy-id-1", "accessPolicy-id-1", criterion); + + // act + var result = client.uiApi().getContractDefinitionPage(); + + // assert + var contractDefinitions = result.getContractDefinitions(); + assertThat(contractDefinitions).hasSize(1); + var contractDefinition = contractDefinitions.get(0); + assertThat(contractDefinition.getContractDefinitionId()).isEqualTo("contractDefinition-id-1"); + assertThat(contractDefinition.getContractPolicyId()).isEqualTo("contractPolicy-id-1"); + assertThat(contractDefinition.getAccessPolicyId()).isEqualTo("accessPolicy-id-1"); + assertThat(contractDefinition.getAssetSelector()).hasSize(1); + + var criterionEntry = contractDefinition.getAssetSelector().get(0); + assertThat(criterionEntry.getOperandLeft()).isEqualTo("exampleLeft1"); + assertThat(criterionEntry.getOperator()).isEqualTo(UiCriterionOperator.EQ); + assertThat(criterionEntry.getOperandRight().getType()).isEqualTo(UiCriterionLiteralType.VALUE); + assertThat(criterionEntry.getOperandRight().getValue()).isEqualTo("abc"); + } + + @Test + void contractDefinitionPageSorting(ContractDefinitionService contractDefinitionService) { + // arrange + var client = TestUtils.edcClient(); + createContractDefinition( + contractDefinitionService, + "contractDefinition-id-1", + "contractPolicy-id-1", + "accessPolicy-id-1", + new Criterion("exampleLeft1", "=", "abc"), + 1628956800000L); + createContractDefinition( + contractDefinitionService, + "contractDefinition-id-2", + "contractPolicy-id-2", + "accessPolicy-id-2", + new Criterion("exampleLeft1", "=", "abc"), + 1628956801000L); + createContractDefinition( + contractDefinitionService, + "contractDefinition-id-3", + "contractPolicy-id-3", + "accessPolicy-id-3", + new Criterion("exampleLeft1", "=", "abc"), + 1628956802000L); + + // act + var result = client.uiApi().getContractDefinitionPage(); + + // assert + assertThat(result.getContractDefinitions()) + .extracting(ContractDefinitionEntry::getContractPolicyId) + .containsExactly("contractPolicy-id-3", "contractPolicy-id-2", "contractPolicy-id-1"); + + } + + @Test + void testContractDefinitionCreation(ContractDefinitionService contractDefinitionService) { + // arrange + var client = TestUtils.edcClient(); + var criterion = new UiCriterion( + "exampleLeft1", + UiCriterionOperator.EQ, + new UiCriterionLiteral(UiCriterionLiteralType.VALUE, "test", null)); + + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId("contractDefinition-id-1") + .contractPolicyId("contractPolicy-id-1") + .accessPolicyId("accessPolicy-id-1") + .assetSelector(List.of(criterion)) + .build(); + + // act + var response = client.uiApi().createContractDefinition(contractDefinition); + + // assert + assertThat(response).isNotNull(); + var contractDefinitions = contractDefinitionService.query(QuerySpec.max()).getContent().toList(); + assertThat(contractDefinitions).hasSize(1); + var contractDefinitionEntry = contractDefinitions.get(0); + assertThat(contractDefinitionEntry.getId()).isEqualTo("contractDefinition-id-1"); + assertThat(contractDefinitionEntry.getContractPolicyId()).isEqualTo("contractPolicy-id-1"); + assertThat(contractDefinitionEntry.getAccessPolicyId()).isEqualTo("accessPolicy-id-1"); + + var criterionEntry = contractDefinition.getAssetSelector().get(0); + assertThat(criterionEntry.getOperandLeft()).isEqualTo("exampleLeft1"); + assertThat(criterionEntry.getOperator()).isEqualTo(UiCriterionOperator.EQ); + assertThat(criterionEntry.getOperandRight().getType()).isEqualTo(UiCriterionLiteralType.VALUE); + assertThat(criterionEntry.getOperandRight().getValue()).isEqualTo("test"); + } + + @Test + void testDeleteContractDefinition(ContractDefinitionService contractDefinitionService) { + // arrange + var client = TestUtils.edcClient(); + var criterion = new Criterion("exampleLeft1", "=", "exampleRight1"); + createContractDefinition(contractDefinitionService, "contractDefinition-id-1", "contractPolicy-id-1", "accessPolicy-id-1", criterion); + assertThat(contractDefinitionService.query(QuerySpec.max()).getContent().toList()).hasSize(1); + var contractDefinition = contractDefinitionService.query(QuerySpec.max()).getContent().toList().get(0); + + // act + var response = client.uiApi().deleteContractDefinition(contractDefinition.getId()); + + // assert + assertThat(response.getId()).isEqualTo(contractDefinition.getId()); + assertThat(contractDefinitionService.query(QuerySpec.max()).getContent()).isEmpty(); + } + + private void createContractDefinition( + ContractDefinitionService contractDefinitionService, + String contractDefinitionId, + String contractPolicyId, + String accessPolicyId, + Criterion criteria, + long createdAt + ) { + + var contractDefinition = ContractDefinition.Builder.newInstance() + .id(contractDefinitionId) + .contractPolicyId(contractPolicyId) + .accessPolicyId(accessPolicyId) + .assetsSelector(List.of(criteria)) + .createdAt(createdAt) + .build(); + contractDefinitionService.create(contractDefinition); + } + + private void createContractDefinition( + ContractDefinitionService contractDefinitionService, + String contractDefinitionId, + String contractPolicyId, + String accessPolicyId, + Criterion criteria + ) { + var contractDefinition = ContractDefinition.Builder.newInstance() + .id(contractDefinitionId) + .contractPolicyId(contractPolicyId) + .accessPolicyId(accessPolicyId) + .assetsSelector(List.of(criteria)) + .build(); + contractDefinitionService.create(contractDefinition); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapperTest.java new file mode 100644 index 000000000..e4a31a9d9 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionLiteralMapperTest.java @@ -0,0 +1,55 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteral; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteralType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class CriterionLiteralMapperTest { + private CriterionLiteralMapper criterionLiteralMapper; + + @BeforeEach + void setup() { + criterionLiteralMapper = new CriterionLiteralMapper(); + } + + @Test + void testBuildUiCriterionLiteral_String() { + String value = "testValue"; + UiCriterionLiteral literal = criterionLiteralMapper.buildUiCriterionLiteral(value); + + assertThat(literal.getType()).isEqualTo(UiCriterionLiteralType.VALUE); + assertThat(literal.getValue()).isEqualTo(value); + assertThat(literal.getValueList()).isNull(); + } + + @Test + void testBuildUiCriterionLiteral_StringList() { + List valueList = Arrays.asList("value1", "value2", null); + UiCriterionLiteral literal = criterionLiteralMapper.buildUiCriterionLiteral(valueList); + + assertThat(literal.getType()).isEqualTo(UiCriterionLiteralType.VALUE_LIST); + assertThat(literal.getValueList()).containsExactly("value1", "value2", null); + assertThat(literal.getValue()).isNull(); + } + + @Test + void testGetValue_String() { + String value = "testValue"; + UiCriterionLiteral literal = UiCriterionLiteral.ofValue(value); + assertThat(criterionLiteralMapper.getValue(literal)).isEqualTo(value); + } + + @Test + void testGetValue_StringList() { + List valueList = Arrays.asList("value1", "value2"); + UiCriterionLiteral literal = UiCriterionLiteral.ofValueList(valueList); + assertThat(criterionLiteralMapper.getValue(literal)).isEqualTo(valueList); + } +} + diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapperTest.java new file mode 100644 index 000000000..367408088 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionMapperTest.java @@ -0,0 +1,44 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterion; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteral; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionOperator; +import org.eclipse.edc.spi.query.Criterion; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CriterionMapperTest { + private CriterionMapper criterionMapper; + + @BeforeEach + void setup() { + criterionMapper = new CriterionMapper(new CriterionOperatorMapper(), new CriterionLiteralMapper()); + } + + @Test + void testMappingFromCriterionToDto() { + Criterion criterion = new Criterion("left", "=", "right"); + UiCriterion dto = criterionMapper.buildUiCriterion(criterion); + + assertThat(dto.getOperandLeft()).isEqualTo("left"); + assertThat(dto.getOperator()).isEqualTo(UiCriterionOperator.EQ); + assertThat(dto.getOperandRight().getValue()).isEqualTo("right"); + } + + @Test + void testMappingFromDtoToCriterion() { + UiCriterion dto = new UiCriterion(); + dto.setOperandLeft("left"); + dto.setOperator(UiCriterionOperator.EQ); + dto.setOperandRight(UiCriterionLiteral.ofValue("right")); + + Criterion criterion = criterionMapper.buildCriterion(dto); + + assertThat(criterion.getOperandLeft()).isEqualTo("left"); + assertThat(criterion.getOperator()).isEqualTo("="); + assertThat(criterion.getOperandRight()).isEqualTo("right"); + } +} + diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapperTest.java new file mode 100644 index 000000000..d2915f2c9 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/CriterionOperatorMapperTest.java @@ -0,0 +1,38 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions; + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionOperator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class CriterionOperatorMapperTest { + private CriterionOperatorMapper criterionOperatorMapper; + + @BeforeEach + void setup() { + criterionOperatorMapper = new CriterionOperatorMapper(); + } + + @Test + void testCaseInsensitivity() { + assertThat(criterionOperatorMapper.getUiCriterionOperator("lIKe")).isEqualTo(UiCriterionOperator.LIKE); + } + + @Test + void testMappings() { + assertThat(criterionOperatorMapper.getUiCriterionOperator("=")).isEqualTo(UiCriterionOperator.EQ); + assertThat(criterionOperatorMapper.getUiCriterionOperator("like")).isEqualTo(UiCriterionOperator.LIKE); + assertThat(criterionOperatorMapper.getUiCriterionOperator("in")).isEqualTo(UiCriterionOperator.IN); + assertThat(criterionOperatorMapper.getCriterionOperator(UiCriterionOperator.EQ)).isEqualTo("="); + assertThat(criterionOperatorMapper.getCriterionOperator(UiCriterionOperator.LIKE)).isEqualTo("like"); + assertThat(criterionOperatorMapper.getCriterionOperator(UiCriterionOperator.IN)).isEqualTo("in"); + + // Ensures the mapping isn't forgotten in the future + Arrays.stream(UiCriterionOperator.values()).forEach(criterionOperatorMapper::getCriterionOperator); + } + +} + diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateServiceTest.java new file mode 100644 index 000000000..fbc022ea2 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateServiceTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations; + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationSimplifiedState; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +class ContractNegotiationStateServiceTest { + ContractNegotiationStateService contractNegotiationStateService; + + @BeforeEach + void setup() { + contractNegotiationStateService = new ContractNegotiationStateService(); + } + + @Test + void testTerminating() { + // Edge case, terminating is not considered "RUNNING" anymore. + int code = ContractNegotiationStates.TERMINATING.code(); + var result = contractNegotiationStateService.buildContractNegotiationState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("TERMINATING"); + assertThat(result.getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.TERMINATED); + } + + @Test + void testTerminated() { + int code = ContractNegotiationStates.TERMINATED.code(); + var result = contractNegotiationStateService.buildContractNegotiationState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("TERMINATED"); + assertThat(result.getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.TERMINATED); + } + + @Test + void testRunning() { + int code = ContractNegotiationStates.INITIAL.code(); + var result = contractNegotiationStateService.buildContractNegotiationState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("INITIAL"); + assertThat(result.getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.IN_PROGRESS); + } + + @Test + void testFinalized() { + int code = ContractNegotiationStates.FINALIZED.code(); + var result = contractNegotiationStateService.buildContractNegotiationState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("FINALIZED"); + assertThat(result.getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + } + + @Test + void testCustomRunning() { + int code = 299; + var result = contractNegotiationStateService.buildContractNegotiationState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("CUSTOM"); + assertThat(result.getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.IN_PROGRESS); + } + + @Test + void testCustomOk() { + int code = 2000; + var result = contractNegotiationStateService.buildContractNegotiationState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("CUSTOM"); + assertThat(result.getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java new file mode 100644 index 000000000..543513525 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.wrapper.TestUtils; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.connector.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.transfer.spi.types.DataRequest; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.service.spi.result.ServiceResult; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; +import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.PROVIDER; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ApiTest +@ExtendWith(EdcExtension.class) +class DashboardPageApiServiceTest { + EdcClient client; + AssetIndex assetIndex; + PolicyDefinitionService policyDefinitionService; + TransferProcessService transferProcessService; + ContractNegotiationStore contractNegotiationStore; + ContractDefinitionService contractDefinitionService; + private Random random; + + @BeforeEach + void setUp(EdcExtension extension) { + assetIndex = mock(AssetIndex.class); + extension.registerServiceMock(AssetIndex.class, assetIndex); + + policyDefinitionService = mock(PolicyDefinitionService.class); + extension.registerServiceMock(PolicyDefinitionService.class, policyDefinitionService); + + transferProcessService = mock(TransferProcessService.class); + extension.registerServiceMock(TransferProcessService.class, transferProcessService); + + contractNegotiationStore = mock(ContractNegotiationStore.class); + extension.registerServiceMock(ContractNegotiationStore.class, contractNegotiationStore); + + contractDefinitionService = mock(ContractDefinitionService.class); + extension.registerServiceMock(ContractDefinitionService.class, contractDefinitionService); + + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + random = new Random(); + } + + + @Test + void testKpis() { + // arrange + mockAmounts( + repeat(7, this::mockAsset), + repeat(8, this::mockPolicyDefinition), + repeat(9, this::mockContractDefinition), + List.of( + mockContractNegotiation(1, CONSUMER), + mockContractNegotiation(2, PROVIDER), + mockContractNegotiation(3, PROVIDER), + mockContractNegotiationInProgress(CONSUMER), + mockContractNegotiationInProgress(PROVIDER) + ), + flat(List.of( + repeat(1, () -> mockTransferProcess(1, TransferProcessStates.REQUESTING.code())), + repeat(2, () -> mockTransferProcess(1, TransferProcessStates.TERMINATED.code())), + repeat(3, () -> mockTransferProcess(1, TransferProcessStates.COMPLETED.code())), + repeat(4, () -> mockTransferProcess(2, TransferProcessStates.REQUESTING.code())), + repeat(5, () -> mockTransferProcess(2, TransferProcessStates.TERMINATED.code())), + repeat(6, () -> mockTransferProcess(2, TransferProcessStates.COMPLETED.code())) + )) + ); + + // act + var dashboardPage = client.uiApi().getDashboardPage(); + assertThat(dashboardPage.getNumAssets()).isEqualTo(7); + assertThat(dashboardPage.getNumPolicies()).isEqualTo(8); + assertThat(dashboardPage.getNumContractDefinitions()).isEqualTo(9); + assertThat(dashboardPage.getNumContractAgreementsConsuming()).isEqualTo(1); + assertThat(dashboardPage.getNumContractAgreementsProviding()).isEqualTo(2); + assertThat(dashboardPage.getTransferProcessesConsuming().getNumTotal()).isEqualTo(1 + 2 + 3); + assertThat(dashboardPage.getTransferProcessesConsuming().getNumRunning()).isEqualTo(1); + assertThat(dashboardPage.getTransferProcessesConsuming().getNumError()).isEqualTo(2); + assertThat(dashboardPage.getTransferProcessesConsuming().getNumOk()).isEqualTo(3); + assertThat(dashboardPage.getTransferProcessesProviding().getNumTotal()).isEqualTo(4 + 5 + 6); + assertThat(dashboardPage.getTransferProcessesProviding().getNumRunning()).isEqualTo(4); + assertThat(dashboardPage.getTransferProcessesProviding().getNumError()).isEqualTo(5); + assertThat(dashboardPage.getTransferProcessesProviding().getNumOk()).isEqualTo(6); + } + + @Test + void testConnectorMetadata() { + // arrange + mockAmounts(List.of(), List.of(), List.of(), List.of(), List.of()); + + // act + var dashboardPage = client.uiApi().getDashboardPage(); + + // assert + assertThat(dashboardPage.getConnectorParticipantId()).isEqualTo("my-edc-participant-id"); + assertThat(dashboardPage.getConnectorDescription()).isEqualTo("My Connector Description"); + assertThat(dashboardPage.getConnectorTitle()).isEqualTo("My Connector"); + assertThat(dashboardPage.getConnectorEndpoint()).isEqualTo(TestUtils.PROTOCOL_ENDPOINT); + assertThat(dashboardPage.getConnectorCuratorName()).isEqualTo("My Org"); + assertThat(dashboardPage.getConnectorCuratorUrl()).isEqualTo("https://connector.my-org"); + assertThat(dashboardPage.getConnectorMaintainerName()).isEqualTo("Maintainer Org"); + assertThat(dashboardPage.getConnectorMaintainerUrl()).isEqualTo("https://maintainer-org"); + + assertThat(dashboardPage.getConnectorDapsConfig()).isNotNull(); + assertThat(dashboardPage.getConnectorDapsConfig().getTokenUrl()).isEqualTo("https://token-url.daps"); + assertThat(dashboardPage.getConnectorDapsConfig().getJwksUrl()).isEqualTo("https://jwks-url.daps"); + + assertThat(dashboardPage.getConnectorMiwConfig()).isNotNull(); + assertThat(dashboardPage.getConnectorMiwConfig().getAuthorityId()).isEqualTo("my-authority-id"); + assertThat(dashboardPage.getConnectorMiwConfig().getUrl()).isEqualTo("https://miw"); + assertThat(dashboardPage.getConnectorMiwConfig().getTokenUrl()).isEqualTo("https://token.miw"); + } + + private Asset mockAsset() { + return mock(Asset.class); + } + + private PolicyDefinition mockPolicyDefinition() { + return mock(PolicyDefinition.class); + } + + private ContractDefinition mockContractDefinition() { + return mock(ContractDefinition.class); + } + + private ContractNegotiation mockContractNegotiation(int contract, ContractNegotiation.Type type) { + var contractAgreement = mock(ContractAgreement.class); + when(contractAgreement.getId()).thenReturn("ca-" + contract); + + var contractNegotiation = mock(ContractNegotiation.class); + when(contractNegotiation.getType()).thenReturn(type); + when(contractNegotiation.getContractAgreement()).thenReturn(contractAgreement); + + return contractNegotiation; + } + + private ContractNegotiation mockContractNegotiationInProgress(ContractNegotiation.Type type) { + var contractNegotiation = mock(ContractNegotiation.class); + when(contractNegotiation.getType()).thenReturn(type); + return contractNegotiation; + } + + private TransferProcess mockTransferProcess(int contractId, int state) { + DataRequest dataRequest = mock(DataRequest.class); + when(dataRequest.getContractId()).thenReturn("ca-" + contractId); + + TransferProcess transferProcess = mock(TransferProcess.class); + when(transferProcess.getId()).thenReturn(String.valueOf(random.nextInt())); + when(transferProcess.getDataRequest()).thenReturn(dataRequest); + when(transferProcess.getState()).thenReturn(state); + return transferProcess; + } + + private void mockAmounts( + List assets, + List policyDefinitions, + List contractDefinitions, + List contractNegotiations, + List transferProcesses + ) { + when(assetIndex.queryAssets(eq(QuerySpec.max()))).thenAnswer(i -> assets.stream()); + when(transferProcessService.query(eq(QuerySpec.max()))) + .thenAnswer(i -> ServiceResult.success(transferProcesses.stream())); + when(policyDefinitionService.query(eq(QuerySpec.max()))) + .thenAnswer(i -> ServiceResult.success(policyDefinitions.stream())); + when(contractNegotiationStore.queryNegotiations(eq(QuerySpec.max()))) + .thenAnswer(i -> contractNegotiations.stream()); + when(contractDefinitionService.query(eq(QuerySpec.max()))) + .thenAnswer(i -> ServiceResult.success(contractDefinitions.stream())); + } + + private List repeat(int times, Supplier supplier) { + return IntStream.range(0, times).mapToObj(i -> supplier.get()).toList(); + } + + private List flat(Collection> collections) { + return collections.stream().flatMap(Collection::stream).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java new file mode 100644 index 000000000..ad371f0ae --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.policy; + + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionDto; +import de.sovity.edc.client.gen.model.UiPolicyConstraint; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyLiteral; +import de.sovity.edc.client.gen.model.UiPolicyLiteralType; +import de.sovity.edc.ext.wrapper.TestUtils; +import lombok.SneakyThrows; +import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.entity.Entity; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class PolicyDefinitionApiServiceTest { + EdcClient client; + + UiPolicyConstraint constraint = UiPolicyConstraint.builder() + .left("a") + .operator(OperatorDto.EQ) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value("b") + .build()) + .build(); + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void test_create_list() { + // arrange + createPolicyDefinition("my-policy-def-1"); + + // act + var response = client.uiApi().getPolicyDefinitionPage(); + + // assert + var policyDefinitions = response.getPolicies(); + assertThat(policyDefinitions).hasSize(2); + var policyDefinition = policyDefinitions.stream() + .filter(it -> it.getPolicyDefinitionId().equals("my-policy-def-1")) + .findFirst().get(); + assertThat(policyDefinition.getPolicyDefinitionId()).isEqualTo("my-policy-def-1"); + assertThat(policyDefinition.getPolicy().getConstraints()).hasSize(1); + + var constraintEntry = policyDefinition.getPolicy().getConstraints().get(0); + assertThat(constraintEntry).usingRecursiveComparison().isEqualTo(constraint); + } + + @Test + void test_sorting(PolicyDefinitionService policyDefinitionService) { + // arrange + createPolicyDefinition(policyDefinitionService, "my-policy-def-2", 1628956802000L); + createPolicyDefinition(policyDefinitionService, "my-policy-def-0", 1628956800000L); + createPolicyDefinition(policyDefinitionService, "my-policy-def-1", 1628956801000L); + + // act + var result = client.uiApi().getPolicyDefinitionPage(); + + // assert + assertThat(result.getPolicies()) + .extracting(PolicyDefinitionDto::getPolicyDefinitionId) + .containsExactly("always-true", "my-policy-def-2", "my-policy-def-1", "my-policy-def-0"); + } + + @Test + void test_delete(PolicyDefinitionService policyDefinitionService) { + // arrange + createPolicyDefinition("my-policy-def-1"); + assertThat(policyDefinitionService.query(QuerySpec.max()).getContent().toList()) + .extracting(Entity::getId).contains("always-true", "my-policy-def-1"); + + // act + var response = client.uiApi().deletePolicyDefinition("my-policy-def-1"); + + // assert + assertThat(response.getId()).isEqualTo("my-policy-def-1"); + assertThat(policyDefinitionService.query(QuerySpec.max()).getContent()) + .extracting(Entity::getId).containsExactly("always-true"); + } + + private void createPolicyDefinition(String policyDefinitionId) { + var policy = new UiPolicyCreateRequest(List.of(constraint)); + var policyDefinition = new PolicyDefinitionCreateRequest(policyDefinitionId, policy); + client.uiApi().createPolicyDefinition(policyDefinition); + } + + @SneakyThrows + private void createPolicyDefinition(PolicyDefinitionService policyDefinitionService, String policyDefinitionId, long createdAt) { + createPolicyDefinition(policyDefinitionId); + var policyDefinition = policyDefinitionService.findById(policyDefinitionId); + + // Forcefully overwrite createdAt + var createdAtField = Entity.class.getDeclaredField("createdAt"); + createdAtField.setAccessible(true); + createdAtField.set(policyDefinition, createdAt); + policyDefinitionService.update(policyDefinition); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java new file mode 100644 index 000000000..88b17cc8b --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractAgreementDirection; +import de.sovity.edc.ext.wrapper.TestUtils; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.text.ParseException; + +import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createConsumingTransferProcesses; +import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createProvidingTransferProcesses; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class TransferHistoryPageApiServiceTest { + EdcClient client; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void transferHistoryTest( + ContractNegotiationStore negotiationStore, + TransferProcessStore transferProcessStore, + AssetService assetStore + ) throws ParseException { + // arrange + createProvidingTransferProcesses(negotiationStore, transferProcessStore, assetStore); + createConsumingTransferProcesses(negotiationStore, transferProcessStore); + + // act + var actual = client.uiApi().getTransferHistoryPage().getTransferEntries(); + + // assert for consuming request entry + var consumingProcess = actual.get(0); + assertThat(consumingProcess.getTransferProcessId()).isEqualTo(TransferProcessTestUtils.CONSUMING_TRANSFER_PROCESS_ID); + assertThat(consumingProcess.getAssetId()).isEqualTo(TransferProcessTestUtils.CONSUMING_ASSET_ID); + assertThat(consumingProcess.getCounterPartyConnectorEndpoint()).isEqualTo(TransferProcessTestUtils.COUNTER_PARTY_ADDRESS); + assertThat(consumingProcess.getCounterPartyParticipantId()).isEqualTo(TransferProcessTestUtils.COUNTER_PARTY_ID); + assertThat(consumingProcess.getContractAgreementId()).isEqualTo(TransferProcessTestUtils.CONSUMING_CONTRACT_ID); + assertThat(consumingProcess.getDirection()).isEqualTo(ContractAgreementDirection.CONSUMING); + assertThat(consumingProcess.getState().getCode()).isEqualTo(800); + assertThat(consumingProcess.getAssetName()).isEqualTo(TransferProcessTestUtils.CONSUMING_ASSET_ID); + assertThat(consumingProcess.getErrorMessage()).isEmpty(); + + // assert for providing request entry + var providingProcess = actual.get(1); + assertThat(providingProcess.getTransferProcessId()).isEqualTo(TransferProcessTestUtils.PROVIDING_TRANSFER_PROCESS_ID); + assertThat(providingProcess.getAssetId()).isEqualTo(TransferProcessTestUtils.PROVIDING_ASSET_ID); + assertThat(providingProcess.getCounterPartyConnectorEndpoint()).isEqualTo(TransferProcessTestUtils.COUNTER_PARTY_ADDRESS); + assertThat(providingProcess.getCounterPartyParticipantId()).isEqualTo(TransferProcessTestUtils.COUNTER_PARTY_ID); + assertThat(providingProcess.getContractAgreementId()).isEqualTo(TransferProcessTestUtils.PROVIDING_CONTRACT_ID); + assertThat(providingProcess.getDirection()).isEqualTo(ContractAgreementDirection.PROVIDING); + assertThat(providingProcess.getState().getCode()).isEqualTo(800); + assertThat(providingProcess.getAssetName()).isEqualTo(TransferProcessTestUtils.PROVIDING_ASSET_NAME); + assertThat(providingProcess.getErrorMessage()).isEqualTo("TransferProcessManager: attempt #8 failed to send transfer"); + } + + @Test + void transferProcessAssetTest_providing( + ContractNegotiationStore negotiationStore, + TransferProcessStore transferProcessStore, + AssetService assetStore + ) throws ParseException { + // arrange + createProvidingTransferProcesses(negotiationStore, transferProcessStore, assetStore); + + // act + var result = client.uiApi().getTransferProcessAsset(TransferProcessTestUtils.PROVIDING_TRANSFER_PROCESS_ID); + + // assert for the order of entries + assertThat(result.getAssetId()).isEqualTo(TransferProcessTestUtils.PROVIDING_ASSET_ID); + assertThat(result.getTitle()).isEqualTo(TransferProcessTestUtils.PROVIDING_ASSET_NAME); + } + + @Test + void transferProcessAssetTest_consuming( + ContractNegotiationStore negotiationStore, + TransferProcessStore transferProcessStore + ) throws ParseException { + // arrange + createConsumingTransferProcesses(negotiationStore, transferProcessStore); + + // act + var result = client.uiApi().getTransferProcessAsset(TransferProcessTestUtils.CONSUMING_TRANSFER_PROCESS_ID); + + // assert for the order of entries + assertThat(result.getAssetId()).isEqualTo(TransferProcessTestUtils.CONSUMING_ASSET_ID); + assertThat(result.getTitle()).isEqualTo(TransferProcessTestUtils.CONSUMING_ASSET_ID); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java new file mode 100644 index 000000000..65ba76323 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.wrapper.TestUtils; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.text.ParseException; + +import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createConsumingTransferProcesses; +import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createProvidingTransferProcesses; +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class TransferProcessAssetApiServiceTest { + EdcClient client = TestUtils.edcClient(); + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void testProviderTransferProcess( + ContractNegotiationStore negotiationStore, + TransferProcessStore transferProcessStore, + AssetService assetStore, + Monitor monitor + ) throws ParseException { + monitor.info("Hello World from TransferProcessAssetApiServiceTest#testProviderTransferProcess!"); + // arrange + createProvidingTransferProcesses(negotiationStore, transferProcessStore, assetStore); + + // act + var providerAssetResult = client.uiApi().getTransferProcessAsset(TransferProcessTestUtils.PROVIDING_TRANSFER_PROCESS_ID); + + // assert + assertThat(providerAssetResult.getAssetId()).isEqualTo(TransferProcessTestUtils.PROVIDING_ASSET_ID); + assertThat(providerAssetResult.getTitle()).isEqualTo(TransferProcessTestUtils.PROVIDING_ASSET_NAME); + } + + @Test + void testConsumerTransferProcess(ContractNegotiationStore negotiationStore, + TransferProcessStore transferProcessStore) throws ParseException { + // arrange + createConsumingTransferProcesses(negotiationStore, transferProcessStore); + + // act + var consumerAssetResult = client.uiApi().getTransferProcessAsset(TransferProcessTestUtils.CONSUMING_TRANSFER_PROCESS_ID); + + // assert + assertThat(consumerAssetResult.getAssetId()).isEqualTo(TransferProcessTestUtils.CONSUMING_ASSET_ID); + assertThat(consumerAssetResult.getTitle()).isEqualTo(TransferProcessTestUtils.CONSUMING_ASSET_ID); + } + +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateServiceTest.java new file mode 100644 index 000000000..977244ec3 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessStateServiceTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +class TransferProcessStateServiceTest { + TransferProcessStateService transferProcessStateService; + + @BeforeEach + void setup() { + transferProcessStateService = new TransferProcessStateService(); + } + + @Test + void testError() { + int code = TransferProcessStates.TERMINATED.code(); + var result = transferProcessStateService.buildTransferProcessState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("TERMINATED"); + assertThat(result.getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.ERROR); + } + + @Test + void testRunning() { + int code = TransferProcessStates.INITIAL.code(); + var result = transferProcessStateService.buildTransferProcessState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("INITIAL"); + assertThat(result.getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.RUNNING); + } + + @Test + void testOk() { + int code = TransferProcessStates.COMPLETED.code(); + var result = transferProcessStateService.buildTransferProcessState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("COMPLETED"); + assertThat(result.getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + } + + @Test + void testDeprovisioning() { + // Edge case, de-provisioning is not considered "RUNNING" anymore. + int code = TransferProcessStates.DEPROVISIONING.code(); + var result = transferProcessStateService.buildTransferProcessState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("DEPROVISIONING"); + assertThat(result.getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + } + + @Test + void testCustomRunning() { + int code = 299; + var result = transferProcessStateService.buildTransferProcessState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("CUSTOM"); + assertThat(result.getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.RUNNING); + } + + @Test + void testCustomOk() { + int code = 2000; + var result = transferProcessStateService.buildTransferProcessState(code); + assertThat(result.getCode()).isEqualTo(code); + assertThat(result.getName()).isEqualTo("CUSTOM"); + assertThat(result.getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java new file mode 100644 index 000000000..5a04d42f4 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.connector.transfer.spi.types.DataRequest; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.asset.Asset; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.UUID; + +public class TransferProcessTestUtils { + public static final String DATA_SINK = "http://my-data-sink/api/stuff"; + public static final String COUNTER_PARTY_ADDRESS = "http://some-other-connector/api/v1/ids/data"; + public static final String COUNTER_PARTY_ID = "some-other-connector"; + public static final String PROVIDING_CONTRACT_ID = "provider-contract:eb934d1f-6582-4bab-85e6-af19a76f7e2b"; + public static final String CONSUMING_CONTRACT_ID = "consumer-contract:f52a5d30-6356-4a55-a75a-3c45d7a88c3e"; + public static final String PROVIDING_ASSET_ID = "my-asset"; + public static final String CONSUMING_ASSET_ID = "some-asset-on-another-connector"; + public static final String PROVIDING_ASSET_NAME = "Test asset"; + public static final String PROVIDING_TRANSFER_PROCESS_ID = "81cdf4cf-8427-480f-9662-8a29d66ddd3b"; + public static final String CONSUMING_TRANSFER_PROCESS_ID = "be0cac12-bb43-420e-aa29-d66bb3d0e0ac"; + + public static void createProvidingTransferProcesses(ContractNegotiationStore store, TransferProcessStore transferProcessStore, AssetService assetStore) throws ParseException { + DataAddress dataAddress = getDataAddress(); + createAsset(assetStore, dataAddress, PROVIDING_ASSET_ID, PROVIDING_ASSET_NAME); + + // preparing providing transfer process + var providerAgreement = createContractAgreement(PROVIDING_CONTRACT_ID, PROVIDING_ASSET_ID); + createContractNegotiation(store, COUNTER_PARTY_ADDRESS, providerAgreement, ContractNegotiation.Type.PROVIDER); + createTransferProcess(PROVIDING_ASSET_ID, + PROVIDING_CONTRACT_ID, + dataAddress, + TransferProcess.Type.PROVIDER, + PROVIDING_TRANSFER_PROCESS_ID, + "2023-07-08", + "TransferProcessManager: attempt #8 failed to send transfer", + transferProcessStore); + } + + public static void createConsumingTransferProcesses(ContractNegotiationStore store, TransferProcessStore transferProcessStore) throws ParseException { + DataAddress dataAddress = getDataAddress(); + + // preparing consuming transfer process + var consumerAgreement = createContractAgreement(CONSUMING_CONTRACT_ID, CONSUMING_ASSET_ID); + createContractNegotiation(store, COUNTER_PARTY_ADDRESS, consumerAgreement, ContractNegotiation.Type.CONSUMER); + createTransferProcess(CONSUMING_ASSET_ID, + CONSUMING_CONTRACT_ID, + dataAddress, + TransferProcess.Type.CONSUMER, + CONSUMING_TRANSFER_PROCESS_ID, + "2023-07-10", + "", + transferProcessStore); + } + + private static DataAddress getDataAddress() { + return DataAddress.Builder.newInstance() + .type("HttpData") + .property("baseUrl", DATA_SINK) + .build(); + } + + private static void createAsset(AssetService assetStore, DataAddress dataAddress, String assetId, String assetName) throws ParseException { + var asset = Asset.Builder.newInstance() + .id(assetId) + .property(Prop.Dcterms.TITLE, assetName) + .createdAt(dateFormatterToLong("2023-06-01")) + .build(); + + assetStore.create(asset, dataAddress); + } + + private static ContractAgreement createContractAgreement( + String agreementId, + String assetId + ) { + return ContractAgreement.Builder.newInstance() + .id(agreementId) + .providerId(UUID.randomUUID().toString()) + .consumerId(UUID.randomUUID().toString()) + .assetId(assetId) + .policy(Policy.Builder.newInstance().build()) + .build(); + } + + private static void createContractNegotiation( + ContractNegotiationStore store, + String counterPartyAddress, + ContractAgreement agreement, + ContractNegotiation.Type type + ) { + var negotiation = ContractNegotiation.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .counterPartyId(COUNTER_PARTY_ID) + .counterPartyAddress(counterPartyAddress) + .protocol(HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .contractAgreement(agreement) + .type(type) + .correlationId(UUID.randomUUID().toString()) + .state(ContractNegotiationStates.FINALIZED.code()) + .build(); + + store.save(negotiation); + } + + private static void createTransferProcess( + String assetId, + String contractId, + DataAddress dataAddress, + TransferProcess.Type type, + String transferProcessId, + String lastUpdateDateForTransferProcess, + String errorMessage, + TransferProcessStore transferProcessStore + ) throws ParseException { + + var dataRequestForTransfer = DataRequest.Builder.newInstance() + .assetId(assetId) + .contractId(contractId) + .dataDestination(dataAddress) + .connectorAddress(COUNTER_PARTY_ADDRESS) + .connectorId(COUNTER_PARTY_ID) + .build(); + + var transferProcess = TransferProcess.Builder.newInstance() + .id(transferProcessId) + .type(type) + .dataRequest(dataRequestForTransfer) + .createdAt(dateFormatterToLong("2023-07-08")) + .updatedAt(dateFormatterToLong(lastUpdateDateForTransferProcess)) + .state(800) + .errorDetail(errorMessage) + .build(); + + transferProcessStore.save(transferProcess); + } + + private static long dateFormatterToLong(String date) throws ParseException { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + return formatter.parse(date).getTime(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java new file mode 100644 index 000000000..4fc72cc3f --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.wrapper.TestUtils; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class KpiApiTest { + EdcClient client; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + @Test + void getKpis() { + // act + var actual = client.useCaseApi().getKpis(); + + // assert + assertThat(actual.getAssetsCount()).isZero(); + assertThat(actual.getContractAgreementsCount()).isZero(); + assertThat(actual.getContractDefinitionsCount()).isZero(); + assertThat(actual.getPoliciesCount()).isEqualTo(1); + assertThat(actual.getTransferProcessDto().getIncomingTransferProcessCounts()).isEmpty(); + assertThat(actual.getTransferProcessDto().getOutgoingTransferProcessCounts()).isEmpty(); + assertThat(actual.getAssetsCount()).isZero(); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java new file mode 100644 index 000000000..73113dd92 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.ext.wrapper.TestUtils; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +class SupportedPolicyApiTest { + EdcClient edcClient; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + edcClient = TestUtils.edcClient(); + } + + @Test + void supportedPolicies() { + // act + var actual = edcClient.useCaseApi().getSupportedFunctions(); + + // assert + assertThat(actual).containsExactly("ALWAYS_TRUE", "https://w3id.org/edc/v0.0.1/ns/inForceDate"); + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/utils/MapUtilsTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/utils/MapUtilsTest.java new file mode 100644 index 000000000..cf8b53fd8 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/utils/MapUtilsTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.utils; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class MapUtilsTest { + + @Test + void mapValues() { + assertThat(MapUtils.mapValues(Map.of(1, "a"), String::toUpperCase)).isEqualTo(Map.of(1, "A")); + } + + @Test + void associateBy() { + assertThat(MapUtils.associateBy(List.of("a"), String::toUpperCase)).isEqualTo(Map.of("A", "a")); + } + + @Test + void reverse() { + assertThat(MapUtils.reverse(Map.of("a", 1))).isEqualTo(Map.of(1, "a")); + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..c367b1c63 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,26 @@ +sovityEdcExtensionsVersion=0.0.1-SNAPSHOT +sovityEdcExtensionGroup=de.sovity.edc.ext +sovityEdcGroup=de.sovity.edc +edcGroup=org.eclipse.edc +edcVersion=0.2.1.2 +tractusGroup=org.eclipse.tractusx.edc +tractusVersion=0.5.3 +assertj=3.23.1 +jsonUnit=3.2.7 +jupiterVersion=5.8.2 +mockitoVersion=4.8.0 +okHttpVersion=4.10.0 +httpMockServerVersion=5.15.0 +jsonVersion=20230618 +restAssured=4.5.0 +flywayVersion=9.0.1 +postgresVersion=42.4.0 +testcontainersVersion=1.17.6 +lombokVersion=1.18.28 +awaitilityVersion=4.2.0 +jettyGroup=org.eclipse.jetty +jettyVersion=11.0.15 +jakartaJsonVersion=2.0.1 + +org.gradle.jvmargs=-Xmx1024m +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..070cb702f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/launchers/.env b/launchers/.env new file mode 100644 index 000000000..19a9d73b5 --- /dev/null +++ b/launchers/.env @@ -0,0 +1,97 @@ +# Default ENV Vars + +# This file will be sourced as bash script: +# - KEY=Value will become KEY=${KEY:-"Value"}, so that ENV Vars can be overwritten by parent docker-compose.yaml. +# - Watch out for escaping issues as values will be surrounded by quotes, and dollar signs must be escaped. + +# Required: Database Connection +MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url +MY_EDC_JDBC_USER=missing-postgresql-user +MY_EDC_JDBC_PASSWORD=missing-postgresql-password + +# Required: Participant ID +MY_EDC_PARTICIPANT_ID=example-connector + +# Required: Connector Metadata +MY_EDC_TITLE=Example Connector +MY_EDC_DESCRIPTION=Default Connector Description Text +MY_EDC_CURATOR_URL="https://example.com" +MY_EDC_CURATOR_NAME="Example GmbH" +MY_EDC_MAINTAINER_URL="https://sovity.de" +MY_EDC_MAINTAINER_NAME="sovity GmbH" + +# Required: Domain Name +MY_EDC_FQDN=example-connector.myorg.com + +# Optional: Add an additional path prefix +MY_EDC_BASE_PATH= + +# Optional: For docker-compose +MY_EDC_PROTOCOL=https:// + +# EDC CONFIG +EDC_JSONLD_HTTPS_ENABLED=true + +EDC_CONNECTOR_NAME=${MY_EDC_PARTICIPANT_ID:-MY_EDC_NAME_KEBAB_CASE} +EDC_PARTICIPANT_ID=${MY_EDC_PARTICIPANT_ID:-MY_EDC_NAME_KEBAB_CASE} + +# Ports and Paths +WEB_HTTP_PORT=11001 +WEB_HTTP_MANAGEMENT_PORT=11002 +WEB_HTTP_PROTOCOL_PORT=11003 +WEB_HTTP_CONTROL_PORT=11004 +WEB_HTTP_PATH=${MY_EDC_BASE_PATH}/api +WEB_HTTP_MANAGEMENT_PATH=${MY_EDC_BASE_PATH}/api/management +WEB_HTTP_PROTOCOL_PATH=${MY_EDC_BASE_PATH}/api/dsp +WEB_HTTP_CONTROL_PATH=${MY_EDC_BASE_PATH}/api/control + +EDC_HOSTNAME=${MY_EDC_FQDN} + +EDC_DSP_CALLBACK_ADDRESS=${MY_EDC_PROTOCOL}${MY_EDC_FQDN}${WEB_HTTP_PROTOCOL_PATH} +EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD=${MY_EDC_PROTOCOL}${MY_EDC_FQDN}${WEB_HTTP_MANAGEMENT_PATH} + +# Flyway Extension: Defaults +EDC_DATASOURCE_DEFAULT_NAME=default +EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_ASSET_NAME=asset +EDC_DATASOURCE_ASSET_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_ASSET_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_ASSET_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_CONTRACTDEFINITION_NAME=contractdefinition +EDC_DATASOURCE_CONTRACTDEFINITION_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_CONTRACTDEFINITION_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_CONTRACTDEFINITION_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_CONTRACTNEGOTIATION_NAME=contractnegotiation +EDC_DATASOURCE_CONTRACTNEGOTIATION_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_CONTRACTNEGOTIATION_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_CONTRACTNEGOTIATION_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_POLICY_NAME=policy +EDC_DATASOURCE_POLICY_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_POLICY_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_POLICY_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_TRANSFERPROCESS_NAME=transferprocess +EDC_DATASOURCE_TRANSFERPROCESS_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_TRANSFERPROCESS_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_TRANSFERPROCESS_PASSWORD=$MY_EDC_JDBC_PASSWORD + +EDC_DATASOURCE_DATAPLANEINSTANCE_NAME=dataplaneinstance +EDC_DATASOURCE_DATAPLANEINSTANCE_URL=$MY_EDC_JDBC_URL +EDC_DATASOURCE_DATAPLANEINSTANCE_USER=$MY_EDC_JDBC_USER +EDC_DATASOURCE_DATAPLANEINSTANCE_PASSWORD=$MY_EDC_JDBC_PASSWORD + +# Oauth default configurations for compatibility with sovity DAPS +EDC_OAUTH_PROVIDER_AUDIENCE=${EDC_OAUTH_TOKEN_URL} +EDC_OAUTH_ENDPOINT_AUDIENCE=idsc:IDS_CONNECTORS_ALL +EDC_AGENT_IDENTITY_KEY=referringConnector + +# This file could contain an entry replacing the EDC_KEYSTORE ENV var +# but for some reason it is required, and EDC won't start up if it isn't configured +# it is created in the Dockerfile +EDC_VAULT=/app/emtpy-properties-file.properties diff --git a/launchers/Dockerfile b/launchers/Dockerfile new file mode 100644 index 000000000..a4dbda971 --- /dev/null +++ b/launchers/Dockerfile @@ -0,0 +1,37 @@ +FROM eclipse-temurin:17-jre-alpine + +# Install curl for healthcheck, bash for entrypoint +RUN apk add --no-cache curl bash +SHELL ["/bin/bash", "-c"] + +# Use a non-root user +RUN adduser -D -H -s /sbin/nologin edc +USER edc:edc + +# Which app.jar to include +ARG CONNECTOR_NAME="sovity-ce" + +# For last-commit-info extension +ARG EDC_LAST_COMMIT_INFO_ARG="The docker container was built outside of github actions and you didn't provide the build arg EDC_LAST_COMMIT_INFO_ARG, so there's no last commit info." +ARG EDC_BUILD_DATE_ARG="The docker container was built outside of github actions and you didn't provide the build arg EDC_BUILD_DATE_ARG, so there's no build date." + +WORKDIR /app +COPY ./launchers/connectors/$CONNECTOR_NAME/build/libs/app.jar /app +COPY ./launchers/logging.properties /app +COPY ./launchers/logging.dev.properties /app +COPY ./launchers/.env /app/.env +RUN touch /app/emtpy-properties-file.properties + +# Replaces var statements so when they are sourced as bash they don't overwrite existing env vars +RUN sed -ri 's/^\s*(\S+)=(.*)$/\1=${\1:-"\2"}/' .env + +ENV EDC_LAST_COMMIT_INFO=$EDC_LAST_COMMIT_INFO_ARG +ENV EDC_BUILD_DATE=$EDC_BUILD_DATE_ARG +ENV JVM_ARGS="" + +COPY ./launchers/docker-entrypoint.sh /app/entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["start"] + +# health status is determined by the availability of the /health endpoint +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:11001/api/check/health diff --git a/launchers/README.md b/launchers/README.md new file mode 100644 index 000000000..cb6363027 --- /dev/null +++ b/launchers/README.md @@ -0,0 +1,62 @@ + +
      +

      + + Logo + + +

      sovity Community Edition EDC:
      Docker Images

      + +

      + Report Bug + · + Request Feature +

      +
      + +## sovity Community Edition EDC Docker Images + +[Eclipse Dataspace Components](https://github.com/eclipse-edc) (EDC) is a framework +for building dataspaces, exchanging data securely with ensured data +sovereignty. + +[sovity](https://sovity.de/) extends the EDC Connector's functionality with extensions to offer +enterprise-ready managed services like "Connector-as-a-Service", out-of-the-box fully configured DAPS +and integrations to existing other dataspace technologies. + +We believe in open source and actively contribute to open source community. Our sovity Community Edition EDC packages +open source EDC Extensions and combines them with [our own open source EDC Extensions](../extensions) to build +ready-to-use EDC Docker Images. + +Together with our [EDC UI](https://github.com/sovity/EDC-UI) Docker Images, it offers several of our extended EDC +functionalities for self-hosting purposes. + +## Different Image Types + +Our sovity Community Edition EDC is built as several docker image variants in different configurations. + +| Docker Image | Type | Purpose | Features | +|----------------------------------------------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [edc-dev](https://github.com/sovity/edc-extensions/pkgs/container/edc-dev) | Devevelopment |
      • Local manual testing
      • Local demos
      |
      • Control- and Data-Plane
      • sovity Community Edition EDC Extensions
      • Management API Auth via API Keys
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | +| [edc-ce](https://github.com/sovity/edc-extensions/pkgs/container/edc-ce) | sovity Community Edition |
      • Self-Deploy a productive sovity EDC
      |
      • Control- and Data-Plane
      • sovity Community Edition EDC Extensions
      • Management API Auth via API Keys
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | +| [edc-ce-mds](https://github.com/sovity/edc-extensions/pkgs/container/edc-ce-mds) | MDS Community Edition |
      • Self-Deploy a productive MDS EDC
      |
      • Control- and Data-Plane
      • sovity Community Edition EDC Extensions
      • Management API Auth via API Keys
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      • Broker Extension
      • Clearing House Extension
      | +| edc-ee | Commercial |
      • Productive use
      • Professional users
      • Our Connector-as-a-Service (CaaS) customers
      • [Request Demo](mailto:contact@sovity.de)
      |
      • Managed Control- and Data Planes, individually scalable
      • Hosted on highly performant infrastructure
      • Management API Auth via Service Accounts
      • Managed User Auth via standalone IAM (SSO)
      • Automatic Dataspace Roll-In, for example to Data Spaces like Catena-X or Mobility Data Space
      • Managed DAPS Authentication
      • Support & Tutorials
      • Automatic updates to newest version and new features
      • Off-the-shelf extensions for use cases available
      • EDC available within minutes
      • Can be combined with Data Space as a Service (DSaaS)
      | + +## Image Tags + +| Tag | Description | +|---------|----------------------------------------------------| +| latest | latest version of our main branch | +| release | latest release of our sovity Community Edition EDC | + +## Configuration + +For available configurations please refer to our [Getting Started Guide](../docs/getting-started/README.md). + +## License + +Apache License 2.0 - see [LICENSE](../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/launchers/common/auth-daps/build.gradle.kts b/launchers/common/auth-daps/build.gradle.kts new file mode 100644 index 000000000..f2323a3d3 --- /dev/null +++ b/launchers/common/auth-daps/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` +} + +val edcVersion: String by project +val edcGroup: String by project + +dependencies { + // OAuth2 IAM + api("${edcGroup}:oauth2-core:${edcVersion}") + api("${edcGroup}:vault-filesystem:${edcVersion}") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/common/auth-mock/build.gradle.kts b/launchers/common/auth-mock/build.gradle.kts new file mode 100644 index 000000000..cf90a2430 --- /dev/null +++ b/launchers/common/auth-mock/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `java-library` +} + +val edcVersion: String by project +val edcGroup: String by project + +dependencies { + // Mock IAM + api("${edcGroup}:iam-mock:${edcVersion}") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/common/base-mds/build.gradle.kts b/launchers/common/base-mds/build.gradle.kts new file mode 100644 index 000000000..6905a27c1 --- /dev/null +++ b/launchers/common/base-mds/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + `java-library` +} + +dependencies { + implementation("logging-house:logging-house-client:0.2.10") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/common/base/build.gradle.kts b/launchers/common/base/build.gradle.kts new file mode 100644 index 000000000..ad391bced --- /dev/null +++ b/launchers/common/base/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + `java-library` +} + +val edcVersion: String by project +val edcGroup: String by project + +dependencies { + // Control-Plane + api("${edcGroup}:control-plane-core:${edcVersion}") + api("${edcGroup}:management-api:${edcVersion}") + api("${edcGroup}:api-observability:${edcVersion}") + api("${edcGroup}:configuration-filesystem:${edcVersion}") + api("${edcGroup}:control-plane-aggregate-services:${edcVersion}") + api("${edcGroup}:http:${edcVersion}") + api("${edcGroup}:dsp:${edcVersion}") + api("${edcGroup}:json-ld:${edcVersion}") + + // Data Management API Key + api("${edcGroup}:auth-tokenbased:${edcVersion}") + + // sovity Extensions Package + api(project(":extensions:sovity-edc-extensions-package")) + api(project(":extensions:postgres-flyway")) + api(project(":extensions:transfer-process-status-checker")) + + // Control-plane to Data-plane + api("${edcGroup}:transfer-data-plane:${edcVersion}") + api("${edcGroup}:data-plane-selector-core:${edcVersion}") + api("${edcGroup}:data-plane-selector-client:${edcVersion}") + + // Data-plane + api("${edcGroup}:data-plane-http:${edcVersion}") + api("${edcGroup}:data-plane-framework:${edcVersion}") + api("${edcGroup}:data-plane-core:${edcVersion}") + api("${edcGroup}:data-plane-util:${edcVersion}") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/common/observability/build.gradle.kts b/launchers/common/observability/build.gradle.kts new file mode 100644 index 000000000..0622ce5f0 --- /dev/null +++ b/launchers/common/observability/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `java-library` +} + +val edcVersion: String by project +val edcGroup: String by project + +dependencies { + // Logging + api("${edcGroup}:monitor-jdk-logger:${edcVersion}") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/connectors/mds-ce/build.gradle.kts b/launchers/connectors/mds-ce/build.gradle.kts new file mode 100644 index 000000000..0779bd143 --- /dev/null +++ b/launchers/connectors/mds-ce/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +val edcVersion: String by project +val edcGroup: String by project + +dependencies { + api(project(":launchers:common:base")) + api(project(":launchers:common:base-mds")) + api(project(":launchers:common:auth-daps")) + api(project(":launchers:common:observability")) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/connectors/sovity-ce/build.gradle.kts b/launchers/connectors/sovity-ce/build.gradle.kts new file mode 100644 index 000000000..c871ae810 --- /dev/null +++ b/launchers/connectors/sovity-ce/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +val edcVersion: String by project +val edcGroup: String by project + +dependencies { + api(project(":launchers:common:base")) + api(project(":launchers:common:auth-daps")) + api(project(":launchers:common:observability")) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/connectors/sovity-dev/build.gradle.kts b/launchers/connectors/sovity-dev/build.gradle.kts new file mode 100644 index 000000000..e4a27c9a9 --- /dev/null +++ b/launchers/connectors/sovity-dev/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +dependencies { + api(project(":launchers:common:base")) + api(project(":launchers:common:auth-mock")) + api(project(":launchers:common:observability")) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/launchers/connectors/test-backend/build.gradle.kts b/launchers/connectors/test-backend/build.gradle.kts new file mode 100644 index 000000000..82a7d8c29 --- /dev/null +++ b/launchers/connectors/test-backend/build.gradle.kts @@ -0,0 +1,28 @@ +val edcVersion: String by project +val edcGroup: String by project + +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +dependencies { + api("${edcGroup}:connector-core:${edcVersion}") + api("${edcGroup}:boot:${edcVersion}") + api("${edcGroup}:http:${edcVersion}") + api("${edcGroup}:api-observability:${edcVersion}") + api(project(":extensions:test-backend-controller")) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup diff --git a/launchers/docker-entrypoint.sh b/launchers/docker-entrypoint.sh new file mode 100755 index 000000000..1f463be66 --- /dev/null +++ b/launchers/docker-entrypoint.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Use bash instead of sh, +# because sh in this image is provided by dash (https://git.kernel.org/pub/scm/utils/dash/dash.git/), +# which seems to eat environment variables containing dashes, +# which are required for some EDC configuration values. + +# Do not set -u to permit unset variables in .env +set -eo pipefail + +# Apply ENV Vars on JAR startup +set -a +source /app/.env +set +a + + +if [[ "x${1:-}" == "xstart" ]]; then + cmd=(java ${JAVA_ARGS:-}) + + if [ "${REMOTE_DEBUG:-n}" = "y" ] || [ "${REMOTE_DEBUG:-false}" = "true" ]; then + cmd+=( + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=${REMOTE_DEBUG_SUSPEND:-n},address=${REMOTE_DEBUG_BIND:-127.0.0.1:5005}" + ) + fi + + logging_config='/app/logging.properties' + if [ "${DEBUG_LOGGING:-n}" = "y" ] || [ "${DEBUG_LOGGING:-false}" = "true" ]; then + logging_config='/app/logging.dev.properties' + fi + + cmd+=( + -Djava.util.logging.config.file=${logging_config} + -jar /app/app.jar + ) +else + cmd=("$@") +fi + +if [ "${REMOTE_DEBUG:-n}" = "y" ] || [ "${REMOTE_DEBUG:-false}" = "true" ]; then + echo "Jar CMD (printing, because REMOTE_DEBUG=y|true): ${cmd[@]}" +fi + +# Use "exec" for termination signals to reach JVM +exec "${cmd[@]}" diff --git a/launchers/logging.dev.properties b/launchers/logging.dev.properties new file mode 100644 index 000000000..3db949d79 --- /dev/null +++ b/launchers/logging.dev.properties @@ -0,0 +1,7 @@ +handlers = java.util.logging.ConsoleHandler +.level = FINE +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s %6$s%n +org.eclipse.dataspaceconnector.level = FINE +org.eclipse.dataspaceconnector.handler = java.util.logging.ConsoleHandler diff --git a/launchers/logging.properties b/launchers/logging.properties new file mode 100644 index 000000000..b4d12f28f --- /dev/null +++ b/launchers/logging.properties @@ -0,0 +1,7 @@ +handlers = java.util.logging.ConsoleHandler +.level = INFO +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s %6$s%n +org.eclipse.dataspaceconnector.level = FINE +org.eclipse.dataspaceconnector.handler = java.util.logging.ConsoleHandler diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..3b7c79e17 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,32 @@ +rootProject.name = "edc-extensions" + +include(":extensions:edc-ui-config") +include(":extensions:last-commit-info") +include(":extensions:policy-always-true") +include(":extensions:policy-referring-connector") +include(":extensions:policy-time-interval") +include(":extensions:postgres-flyway") +include(":extensions:sovity-edc-extensions-package") +include(":extensions:test-backend-controller") +include(":extensions:transfer-process-status-checker") +include(":extensions:wrapper:clients:java-client") +include(":extensions:wrapper:clients:java-client-example") +include(":extensions:wrapper:wrapper") +include(":extensions:wrapper:wrapper-api") +include(":extensions:wrapper:wrapper-common-api") +include(":extensions:wrapper:wrapper-common-mappers") +include(":extensions:wrapper:wrapper-ee-api") +include(":launchers:common:auth-daps") +include(":launchers:common:auth-mock") +include(":launchers:common:base") +include(":launchers:common:base-mds") +include(":launchers:common:observability") +include(":launchers:connectors:mds-ce") +include(":launchers:connectors:sovity-ce") +include(":launchers:connectors:sovity-dev") +include(":launchers:connectors:test-backend") +include(":tests") +include(":utils:catalog-parser") +include(":utils:json-and-jsonld-utils") +include(":utils:test-connector-remote") +include(":utils:test-utils") diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts new file mode 100644 index 000000000..45f2b67a6 --- /dev/null +++ b/tests/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + `java-library` + id("org.gradle.test-retry") version "1.5.7" +} + +val assertj: String by project +val edcVersion: String by project +val edcGroup: String by project +val httpMockServerVersion: String by project +val jsonUnit: String by project +val jupiterVersion: String by project +val lombokVersion: String by project +val mockitoVersion: String by project + +dependencies { + api(project(":launchers:common:base")) + api(project(":launchers:common:auth-mock")) + + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testImplementation(project(":utils:test-utils")) + testImplementation(project(":extensions:test-backend-controller")) + testImplementation(project(":utils:test-connector-remote")) + testImplementation(project(":extensions:wrapper:clients:java-client")) + testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") + testImplementation("org.junit.jupiter:junit-jupiter-params:${jupiterVersion}") + testImplementation("org.mock-server:mockserver-netty:${httpMockServerVersion}") { + // TODO: increase minimum guava version + } + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +tasks.withType { + maxParallelForks = 1 +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup diff --git a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java new file mode 100644 index 000000000..dd0a9916c --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiContractNegotiation; +import de.sovity.edc.client.gen.model.UiContractOffer; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiPolicyConstraint; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyLiteral; +import de.sovity.edc.client.gen.model.UiPolicyLiteralType; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.awaitility.Awaitility; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static org.assertj.core.api.Assertions.assertThat; + +class ApiWrapperDemoTest { + + private static final String PROVIDER_PARTICIPANT_ID = "provider"; + private static final String CONSUMER_PARTICIPANT_ID = "consumer"; + + @RegisterExtension + static EdcExtension providerEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + + private EdcClient providerClient; + private EdcClient consumerClient; + private MockDataAddressRemote dataAddress; + private final String dataOfferData = "expected data 123"; + + private final String dataOfferId = "my-data-offer-2023-11"; + + @BeforeEach + void setup() { + // set up provider EDC + Client + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + providerEdcContext.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // set up consumer EDC + Client + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + consumerEdcContext.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) + dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + } + + @Test + void provide_and_consume() { + // provider: create data offer + createPolicy(); + createAsset(); + createContractDefinition(); + + // consumer: negotiate contract and transfer data + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + var negotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); + negotiation = awaitNegotiationDone(negotiation.getContractNegotiationId()); + initiateTransfer(negotiation); + + // check data sink + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), dataOfferData); + } + + private void createAsset() { + var asset = UiAssetCreateRequest.builder() + .id(dataOfferId) + .title("My Data Offer") + .description("Example Data Offer.") + .version("2023-11") + .language("EN") + .publisherHomepage("https://my-department.my-org.com/my-data-offer") + .licenseUrl("https://my-department.my-org.com/my-data-offer#license") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(dataOfferData) + )) + .build(); + + providerClient.uiApi().createAsset(asset); + } + + private void createPolicy() { + var afterYesterday = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().minusDays(1).toString()) + .build()) + .build(); + + var beforeTomorrow = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.LT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().plusDays(1).toString()) + .build()) + .build(); + + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(dataOfferId) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(afterYesterday, beforeTomorrow)) + .build()) + .build(); + + providerClient.uiApi().createPolicyDefinition(policyDefinition); + } + + private void createContractDefinition() { + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); + + providerClient.uiApi().createContractDefinition(contractDefinition); + } + + private UiContractNegotiation initiateNegotiation(UiDataOffer dataOffer, UiContractOffer contractOffer) { + var negotiationRequest = ContractNegotiationRequest.builder() + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); + + return consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); + } + + private UiContractNegotiation awaitNegotiationDone(String negotiationId) { + var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + ); + + assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + return negotiation; + } + + private void initiateTransfer(UiContractNegotiation negotiation) { + var contractAgreementId = negotiation.getContractAgreementId(); + var transferRequest = InitiateTransferRequest.builder() + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataAddress.getDataSinkProperties()) + .build(); + consumerClient.uiApi().initiateTransfer(transferRequest); + } + + private String getProtocolEndpoint(ConnectorRemote connector) { + return connector.getConfig().getProtocolEndpoint().getUri().toString(); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java new file mode 100644 index 000000000..1fbdce1c0 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -0,0 +1,560 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.TransferHistoryEntry; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiContractNegotiation; +import de.sovity.edc.client.gen.model.UiContractOffer; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseFactory; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.HttpHeaders; +import lombok.val; +import okhttp3.HttpUrl; +import org.awaitility.Awaitility; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.HttpStatusCode; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.OK; +import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.RUNNING; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.BODY; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.MEDIA_TYPE; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.METHOD; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.PATH; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.QUERY_PARAMS; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.mockserver.matchers.Times.once; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.stop.Stop.stopQuietly; + +class DataSourceParameterizationTest { + + private static final String PROVIDER_PARTICIPANT_ID = "provider"; + private static final String CONSUMER_PARTICIPANT_ID = "consumer"; + + @RegisterExtension + static final EdcExtension PROVIDER_EDC_CONTEXT = new EdcExtension(); + @RegisterExtension + static final EdcExtension CONSUMER_EDC_CONTEXT = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = TestDatabaseFactory.getTestDatabase(1); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = TestDatabaseFactory.getTestDatabase(2); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + + private EdcClient providerClient; + private EdcClient consumerClient; + + private final int port = getFreePort(); + private final String sourcePath = "/source/some/path/"; + private final String destinationPath = "/destination/some/path/"; + private final String sourceUrl = "http://localhost:" + port + sourcePath; + private final String destinationUrl = "http://localhost:" + port + destinationPath; + // TODO: remove the test backend dependency? + private ClientAndServer mockServer; + + private static final AtomicInteger DATA_OFFER_INDEX = new AtomicInteger(0); + + record TestCase( + String name, + String id, + String method, + @Nullable String body, + @Nullable String mediaType, + @Nullable String path, + Map> queryParams + ) { + @Override + public String toString() { + return name; + } + } + + @BeforeEach + public void startServer() { + mockServer = ClientAndServer.startClientAndServer(port); + } + + @AfterEach + public void stopServer() { + stopQuietly(mockServer); + } + + @BeforeEach + void setup() { + // set up provider EDC + Client + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + PROVIDER_EDC_CONTEXT.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // set up consumer EDC + Client + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + CONSUMER_EDC_CONTEXT.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + } + + @Test + void canUseTheWorkaroundInCustomTransferRequest() { + // arrange + val testCase = new TestCase( + "", + "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), + HttpMethod.PATCH, + "[]", + "application/json", + "my-endpoint", + Map.of("filter", List.of("a", "b", "c")) + ); + val received = new AtomicBoolean(false); + prepareDataTransferBackends(testCase, () -> received.set(true)); + + createData(testCase); + + // act + val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + val startNegotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); + val negotiation = awaitNegotiationDone(startNegotiation.getContractNegotiationId()); + + String standardBase = "https://w3id.org/edc/v0.0.1/ns/"; + String workaroundBase = "https://sovity.de/workaround/proxy/param/"; + var transferRequestJsonLd = Json.createObjectBuilder() + .add( + Prop.Edc.DATA_DESTINATION, + Json.createObjectBuilder(Map.of( + standardBase + "type", "HttpData", + standardBase + "baseUrl", destinationUrl, + standardBase + "method", "PUT", + workaroundBase + "pathSegments", testCase.path, + workaroundBase + "method", testCase.method, + workaroundBase + "queryParams", "filter=a&filter=b&filter=c", + workaroundBase + "mediaType", testCase.mediaType, + workaroundBase + "body", testCase.body + )).build() + ) + .add(Prop.Edc.CTX + "transferType", Json.createObjectBuilder() + .add(Prop.Edc.CTX + "contentType", "application/octet-stream") + .add(Prop.Edc.CTX + "isFinite", true) + ) + .add(Prop.Edc.CTX + "protocol", HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .add(Prop.Edc.CTX + "managedResources", false) + .build(); + var transferRequest = InitiateCustomTransferRequest.builder() + .contractAgreementId(negotiation.getContractAgreementId()) + .transferProcessRequestJsonLd(JsonUtils.toJson(transferRequestJsonLd)) + .build(); + + val transferId = consumerClient.uiApi().initiateCustomTransfer(transferRequest).getId(); + + awaitTransferCompletion(transferId); + + // assert + TransferHistoryEntry actual = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); + assertThat(actual.getAssetId()).isEqualTo(testCase.id); + assertThat(actual.getTransferProcessId()).isEqualTo(transferId); + assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + + assertThat(received.get()).isTrue(); + } + + private void createData(TestCase testCase) { + createPolicy(testCase); + createAssetWithParameterizedMethod(testCase); + createContractDefinition(testCase); + } + + @Test + void sendWithEdcManagementApi() { + // arrange + val testCase = new TestCase( + "", + "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), + HttpMethod.PATCH, + "[]", + "application/json", + "my-endpoint", + Map.of("filter", List.of("a", "b", "c")) + ); + val received = new AtomicBoolean(false); + prepareDataTransferBackends(testCase, () -> received.set(true)); + + createData(testCase); + + // act + val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + val startNegotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); + val negotiation = awaitNegotiationDone(startNegotiation.getContractNegotiationId()); + + String workaroundBase = "https://sovity.de/workaround/proxy/param/"; + String standardBase = "https://w3id.org/edc/v0.0.1/ns/"; + val transferId = consumerConnector.initiateTransfer( + negotiation.getContractAgreementId(), + testCase.id, + URI.create("http://localhost:21003/api/dsp"), + Json.createObjectBuilder(Map.of( + standardBase + "type", "HttpData", + standardBase + "baseUrl", destinationUrl, + standardBase + "method", "PUT", + workaroundBase + "pathSegments", testCase.path, + workaroundBase + "method", testCase.method, + workaroundBase + "queryParams", "filter=a&filter=b&filter=c", + workaroundBase + "mediaType", testCase.mediaType, + workaroundBase + "body", testCase.body + )).build() + ); + + awaitTransferCompletion(transferId); + + // assert + TransferHistoryEntry actual = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); + assertThat(actual.getAssetId()).isEqualTo(testCase.id); + assertThat(actual.getTransferProcessId()).isEqualTo(transferId); + assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + + assertThat(received.get()).isTrue(); + } + + @Test + void canTransferParameterizedAsset() { + source().forEach(testCase -> { + // arrange + val received = new AtomicBoolean(false); + prepareDataTransferBackends(testCase, () -> received.set(true)); + + createData(testCase); + + // act + val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + val dataOffer = dataOffers.stream().filter(it -> it.getAsset().getAssetId().equals(testCase.id)).findFirst().get(); + val negotiationInit = initiateNegotiation(dataOffer, dataOffer.getContractOffers().get(0)); + val negotiation = awaitNegotiationDone(negotiationInit.getContractNegotiationId()); + val transferId = initiateTransferWithParameters(negotiation, testCase); + + awaitTransferCompletion(transferId); + + // assert + TransferHistoryEntry actual = consumerClient.uiApi() + .getTransferHistoryPage() + .getTransferEntries() + .stream() + .filter(it -> it.getAssetId().equals(testCase.id)) + .findFirst() + .get(); + assertThat(actual.getAssetId()).isEqualTo(testCase.id); + assertThat(actual.getTransferProcessId()).isEqualTo(transferId); + assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + + assertThat(received.get()).isTrue(); + }); + } + + private Stream source() { + val httpMethods = List.of( + HttpMethod.POST, + // HttpMethod.HEAD, + HttpMethod.GET, + HttpMethod.DELETE, + HttpMethod.PUT, + HttpMethod.PATCH, + HttpMethod.OPTIONS + ); + + val paths = Arrays.asList(null, "different/path/segment"); + val queryParameters = List.of( + Map.>of(), + Map.of( + "limit", List.of("10"), + "filter", List.of("a", "b", "c") + ) + ); + + return httpMethods.stream().flatMap(method -> + getBodyOptionsFor(method).stream().flatMap(body -> + paths.stream().flatMap(usePath -> + queryParameters.stream().map(params -> + new TestCase( + method + " body:" + body + " path:" + usePath + " params=" + params, + "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), + method, + body, + body == null ? null : "application/json", + usePath, + params + ))) + )); + } + + @NotNull + private static List getBodyOptionsFor(String method) { + final List useBodyChoices; + val payload = "{ \"somePayload\" : \"" + method + "\" }"; + + if (okhttp3.internal.http.HttpMethod.requiresRequestBody(method)) { + useBodyChoices = List.of(payload); + } else if (!okhttp3.internal.http.HttpMethod.permitsRequestBody(method)) { + useBodyChoices = Collections.singletonList(null); + } else { + useBodyChoices = Arrays.asList(payload, null); + } + return useBodyChoices; + } + + private void prepareDataTransferBackends(TestCase testCase, Runnable onRequestReceived) { + String payload = generateRandomPayload(); + mockServer.reset(); + + val requestDefinition = request(sourcePath).withMethod(testCase.method); + if (testCase.body != null) { + requestDefinition.withBody(testCase.body); + } + if (testCase.path != null) { + requestDefinition.withPath(sourcePath + testCase.path); + } + if (testCase.mediaType != null) { + requestDefinition.withHeader(HttpHeaders.CONTENT_TYPE, testCase.mediaType); + } + if (testCase.queryParams != null) { + requestDefinition.withQueryStringParameters(testCase.queryParams); + } + + + mockServer.when(requestDefinition, once()) + .respond((it) -> new HttpResponse() + .withStatusCode(HttpStatusCode.OK_200.code()) + .withBody(payload, StandardCharsets.UTF_8)); + + mockServer.when(request(destinationPath).withMethod(HttpMethod.PUT)) + .respond((HttpRequest httpRequest) -> { + if (new String(httpRequest.getBodyAsRawBytes()).equals(payload)) { + onRequestReceived.run(); + } + return new HttpResponse().withStatusCode(200); + }); + + mockServer.when(request("/.*")) + .respond((HttpRequest httpRequest) -> { + fail("Unexpected network call"); + return new HttpResponse().withStatusCode(HttpStatusCode.GONE_410.code()); + }); + } + + private static String generateRandomPayload() { + byte[] data = new byte[10]; + new Random().nextBytes(data); + return Base64.getEncoder().encodeToString(data); + } + + private String createAssetWithParameterizedMethod(TestCase testCase) { + val proxyProperties = new HashMap<>(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.BASE_URL, sourceUrl + )); + if (testCase.path != null) { + proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyPath", "true"); + } + if (testCase.body != null) { + proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyBody", "true"); + } + if (testCase.method != null) { + proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyMethod", "true"); + } + if (testCase.queryParams != null) { + proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyQueryParams", "true"); + } + + var asset = UiAssetCreateRequest.builder() + .id(testCase.id) + .title("My Data Offer") + .dataAddressProperties(proxyProperties) + .build(); + + return providerClient.uiApi().createAsset(asset).getId(); + } + + private void createPolicy(TestCase testCase) { + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(testCase.id) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build(); + + providerClient.uiApi().createPolicyDefinition(policyDefinition); + } + + private String createContractDefinition(TestCase testCase) { + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId(testCase.id) + .accessPolicyId(testCase.id) + .contractPolicyId(testCase.id) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(testCase.id) + .build()) + .build())) + .build(); + + return providerClient.uiApi().createContractDefinition(contractDefinition).getId(); + } + + private UiContractNegotiation initiateNegotiation(UiDataOffer dataOffer, UiContractOffer contractOffer) { + var negotiationRequest = ContractNegotiationRequest.builder() + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); + + return consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); + } + + private UiContractNegotiation awaitNegotiationDone(String negotiationId) { + var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + ); + + assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + return negotiation; + } + + private String initiateTransferWithParameters( + UiContractNegotiation negotiation, + TestCase testCase) { + String rootKey = "https://w3id.org/edc/v0.0.1/ns/"; + + val transferProcessProperties = new HashMap(); + + var contractAgreementId = negotiation.getContractAgreementId(); + Map dataSinkProperties = new HashMap<>(); + dataSinkProperties.put(EDC_NAMESPACE + "baseUrl", destinationUrl); + dataSinkProperties.put(EDC_NAMESPACE + "method", HttpMethod.PUT); + dataSinkProperties.put(EDC_NAMESPACE + "type", "HttpData"); // TODO: http proxy + transferProcessProperties.put(rootKey + METHOD, testCase.method); + + if (testCase.body != null) { + dataSinkProperties.put("https://w3id.org/edc/v0.0.1/ns/body", testCase.body); + transferProcessProperties.put(rootKey + BODY, testCase.body); + transferProcessProperties.put(rootKey + MEDIA_TYPE, testCase.mediaType); + transferProcessProperties.put(rootKey + "contentType", testCase.mediaType); + } + + if (testCase.path != null) { + transferProcessProperties.put(rootKey + PATH, testCase.path); + } + + if (!testCase.queryParams.isEmpty()) { + HttpUrl.Builder builder = new HttpUrl.Builder() + .scheme("http") + .host("example.com"); + + for (val multiValueParam : testCase.queryParams.entrySet()) { + for (val singleValue : multiValueParam.getValue()) { + builder.addQueryParameter(multiValueParam.getKey(), singleValue); + } + } + + val allQueryParams = builder.build().encodedQuery(); + + transferProcessProperties.put(rootKey + QUERY_PARAMS, allQueryParams); + } + + var transferRequest = InitiateTransferRequest.builder() + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataSinkProperties) + .transferProcessProperties(transferProcessProperties) + .build(); + return consumerClient.uiApi().initiateTransfer(transferRequest).getId(); + } + + private String getProtocolEndpoint(ConnectorRemote connector) { + return connector.getConfig().getProtocolEndpoint().getUri().toString(); + } + + private void awaitTransferCompletion(String transferId) { + Awaitility.await().atMost(consumerConnector.timeout).until( + () -> consumerClient.uiApi() + .getTransferHistoryPage() + .getTransferEntries() + .stream() + .filter(it -> it.getTransferProcessId().equals(transferId)) + .findFirst() + .map(it -> it.getState().getSimplifiedState()), + it -> it.orElse(RUNNING) != RUNNING + ); + } + +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java new file mode 100644 index 000000000..aad4167c8 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiContractNegotiation; +import de.sovity.edc.client.gen.model.UiContractOffer; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseFactory; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.awaitility.Awaitility; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static org.assertj.core.api.Assertions.assertThat; + +class DataSourceQueryParamsTest { + + private static final String PROVIDER_PARTICIPANT_ID = "provider"; + private static final String CONSUMER_PARTICIPANT_ID = "consumer"; + + @RegisterExtension + static EdcExtension providerEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = TestDatabaseFactory.getTestDatabase(1); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = TestDatabaseFactory.getTestDatabase(2); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + + private EdcClient providerClient; + private EdcClient consumerClient; + private MockDataAddressRemote dataAddress; + private final String encodedParam = "a=%25"; // Unencoded param "a=%" + private final String dataOfferId = "my-data-offer-2023-11"; + + @BeforeEach + void setup() { + // set up provider EDC + Client + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + providerEdcContext.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // set up consumer EDC + Client + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + consumerEdcContext.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) + dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + } + + @Test + void testDirectQuerying() { + // arrange + var expected = "a=%"; + // will be encoded in assertResponseContent before request + var queryParams = new HashMap(); + queryParams.put("a", "%"); + + // act + // assert + validateDataTransferred(dataAddress.getDataSourceQueryParamsUrl(), queryParams, expected); + } + + /** + * This test will fail as soon as the handling of query parameters is fixed in the EDC project + */ + @DisabledOnGithub + @Test + void testQueryParamsDoubleEncoded() { + // arrange + createPolicy(); + createAsset(); + createContractDefinition(); + + // act + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + var negotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); + negotiation = awaitNegotiationDone(negotiation.getContractNegotiationId()); + initiateTransfer(negotiation); + + // assert + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), encodedParam); + } + + private void createAsset() { + var asset = UiAssetCreateRequest.builder() + .id(dataOfferId) + .title("My Data Offer") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, dataAddress.getDataSourceQueryParamsUrl(), + "https://w3id.org/edc/v0.0.1/ns/queryParams", encodedParam + )) + .build(); + + providerClient.uiApi().createAsset(asset); + } + + private void createPolicy() { + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(dataOfferId) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build(); + + providerClient.uiApi().createPolicyDefinition(policyDefinition); + } + + private void createContractDefinition() { + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); + + providerClient.uiApi().createContractDefinition(contractDefinition); + } + + private UiContractNegotiation initiateNegotiation(UiDataOffer dataOffer, UiContractOffer contractOffer) { + var negotiationRequest = ContractNegotiationRequest.builder() + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); + + return consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); + } + + private UiContractNegotiation awaitNegotiationDone(String negotiationId) { + var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + ); + + assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + return negotiation; + } + + private void initiateTransfer(UiContractNegotiation negotiation) { + var contractAgreementId = negotiation.getContractAgreementId(); + var transferRequest = InitiateTransferRequest.builder() + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataAddress.getDataSinkProperties()) + .build(); + consumerClient.uiApi().initiateTransfer(transferRequest); + } + + private String getProtocolEndpoint(ConnectorRemote connector) { + return connector.getConfig().getProtocolEndpoint().getUri().toString(); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java b/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java new file mode 100644 index 000000000..169ef34c1 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.UUID; + +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; + +class ManagementApiTransferTest { + + private static final String PROVIDER_PARTICIPANT_ID = "provider"; + private static final String CONSUMER_PARTICIPANT_ID = "consumer"; + private static final String TEST_BACKEND_TEST_DATA = UUID.randomUUID().toString(); + + @RegisterExtension + static EdcExtension providerEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + private MockDataAddressRemote dataAddress; + + @BeforeEach + void setup() { + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + providerEdcContext.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + consumerEdcContext.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) + dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + } + + @Test + void testDataTransfer() { + // arrange + var assetId = UUID.randomUUID().toString(); + providerConnector.createDataOffer(assetId, dataAddress.getDataSourceUrl(TEST_BACKEND_TEST_DATA)); + + // act + consumerConnector.consumeOffer( + providerConnector.getParticipantId(), + providerConnector.getConfig().getProtocolEndpoint().getUri(), + assetId, + dataAddress.getDataSinkJsonLd()); + + // assert + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), TEST_BACKEND_TEST_DATA); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java b/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java new file mode 100644 index 000000000..dd6d9e7e1 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractAgreementDirection; +import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.ext.wrapper.utils.EdcDateUtils; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.data.TemporalUnitLessThanOffset; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.nio.file.Paths; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.function.Predicate; + +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Test data offers and contracts of an MS8 connector migrated to the current version. + */ +class Ms8ConnectorMigrationTest { + + private static final String PROVIDER_PARTICIPANT_ID = "example-provider"; + private static final String CONSUMER_PARTICIPANT_ID = "example-connector"; + + @RegisterExtension + static EdcExtension providerEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + private EdcClient providerClient; + private EdcClient consumerClient; + private MockDataAddressRemote dataAddress; + + @BeforeEach + void setup() { + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + providerConfig.setProperty("edc.flyway.additional.migration.locations", + "filesystem:%s".formatted(getAbsoluteTestResourcePath("db/additional-test-data/provider"))); + providerEdcContext.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + consumerConfig.setProperty("edc.flyway.additional.migration.locations", + "filesystem:%s".formatted(getAbsoluteTestResourcePath("db/additional-test-data/consumer"))); + consumerEdcContext.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) + dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + } + + @DisabledOnGithub + @Test + void testMs8DataOffer_Properties() { + // arrange + var providerEndpoint = endpoint(providerConnector); + + // act + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerEndpoint); + var asset = first(dataOffers, it -> it.getAsset().getAssetId().equals("first-asset-1.0")).getAsset(); + + // assert + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(asset.getAssetId()).isEqualTo("first-asset-1.0"); + softly.assertThat(asset.getAssetJsonLd()).startsWith("{").endsWith("}"); + softly.assertThat(asset.getCreatorOrganizationName()).isEqualTo("Example GmbH"); + softly.assertThat(asset.getDataCategory()).isEqualTo("Traffic Information"); + softly.assertThat(asset.getDataModel()).isEqualTo("data-model"); + softly.assertThat(asset.getDataSubcategory()).isEqualTo("Accidents"); + softly.assertThat(asset.getDescription()).isEqualTo("My First Asset"); + softly.assertThat(asset.getGeoReferenceMethod()).isEqualTo("geo-ref"); + softly.assertThat(asset.getHttpDatasourceHintsProxyBody()).isFalse(); + softly.assertThat(asset.getHttpDatasourceHintsProxyMethod()).isFalse(); + softly.assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); + softly.assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isFalse(); + softly.assertThat(asset.getKeywords()).containsExactlyInAnyOrder("first", "asset"); + softly.assertThat(asset.getLandingPageUrl()).isEqualTo("https://endpoint-documentation"); + softly.assertThat(asset.getLanguage()).isEqualTo("https://w3id.org/idsa/code/EN"); + softly.assertThat(asset.getLicenseUrl()).isEqualTo("https://standard-license"); + softly.assertThat(asset.getMediaType()).isEqualTo("text/plain"); + softly.assertThat(asset.getTitle()).isEqualTo("First Asset"); + softly.assertThat(asset.getPublisherHomepage()).isEqualTo("https://publisher"); + softly.assertThat(asset.getTransportMode()).isEqualTo("Rail"); + softly.assertThat(asset.getVersion()).isEqualTo("1.0"); + }); + } + + @DisabledOnGithub + @Test + void testMs8ProvidingTransferProcess() { + // arrange + + // act + var providerTransfers = providerClient.uiApi().getTransferHistoryPage().getTransferEntries(); + assertThat(providerTransfers).hasSize(1); + var providerTransfer = providerTransfers.get(0); + + // assert + assertThat(providerTransfer.getAssetId()).isEqualTo("first-asset-1.0"); + assertThat(providerTransfer.getAssetName()).isEqualTo("First Asset"); + assertThat(providerTransfer.getContractAgreementId()).isEqualTo("Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi"); + assertThat(providerTransfer.getCounterPartyConnectorEndpoint()).isEqualTo(endpoint(consumerConnector)); + assertThat(providerTransfer.getCounterPartyParticipantId()).isEqualTo(consumerConnector.getParticipantId()); + assertIsEqualOffsetDateTime(providerTransfer.getCreatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208010855L)); + assertThat(providerTransfer.getDirection()).isEqualTo(ContractAgreementDirection.PROVIDING); + assertThat(providerTransfer.getErrorMessage()).isNull(); + assertIsEqualOffsetDateTime(providerTransfer.getLastUpdatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208010083L)); + assertThat(providerTransfer.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + assertThat(providerTransfer.getTransferProcessId()).isEqualTo("27075fc4-b18f-44e1-8bde-a9f62817dab2"); + } + + private void assertIsEqualOffsetDateTime(OffsetDateTime actual, OffsetDateTime expected) { + assertThat(actual).isCloseTo(expected, new TemporalUnitLessThanOffset(1, ChronoUnit.MINUTES)); + } + + @DisabledOnGithub + @Test + void testMs8ConsumingTransferProcess() { + // arrange + + // act + var consumerTransfers = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries(); + assertThat(consumerTransfers).hasSize(1); + var consumerTransfer = consumerTransfers.get(0); + + // assert + assertThat(consumerTransfer.getAssetId()).isEqualTo("first-asset-1.0"); + assertThat(consumerTransfer.getAssetName()).isEqualTo("first-asset-1.0"); + assertThat(consumerTransfer.getContractAgreementId()).isEqualTo("Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi"); + assertThat(consumerTransfer.getCounterPartyConnectorEndpoint()).isEqualTo(endpoint(providerConnector)); + assertThat(consumerTransfer.getCounterPartyParticipantId()).isEqualTo(providerConnector.getParticipantId()); + assertIsEqualOffsetDateTime(consumerTransfer.getCreatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208008652L)); + assertThat(consumerTransfer.getDirection()).isEqualTo(ContractAgreementDirection.CONSUMING); + assertThat(consumerTransfer.getErrorMessage()).isNull(); + assertIsEqualOffsetDateTime(consumerTransfer.getLastUpdatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208011094L)); + assertThat(consumerTransfer.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + assertThat(consumerTransfer.getTransferProcessId()).isEqualTo("946aadd4-d4bf-47e9-8aea-c2279070e839"); + } + + @Test + void testMs8DataOffer_negotiateAndTransferNewContract() { + // arrange + var assetIds = providerConnector.getAssetIds(); + assertThat(assetIds).contains("second-asset"); + + // act + consumerConnector.consumeOffer( + providerConnector.getParticipantId(), + providerConnector.getConfig().getProtocolEndpoint().getUri(), + "second-asset", + dataAddress.getDataSinkJsonLd()); + + // assert + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), "second-asset-data"); + } + + @Test + void testMs8Contract_transfer() { + // arrange + var assetIds = providerConnector.getAssetIds(); + assertThat(assetIds).contains("second-asset"); + + // act + var transferProcessId = consumerConnector.initiateTransfer( + "Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi", + "first-asset-1.0", + providerConnector.getConfig().getProtocolEndpoint().getUri(), + dataAddress.getDataSinkJsonLd() + ); + + // assert + assertThat(transferProcessId).isNotNull(); + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), "first-asset-data"); + } + + private T first(List items, Predicate predicate) { + return items.stream().filter(predicate).findFirst().get(); + } + + private String endpoint(ConnectorRemote remote) { + return remote.getConfig().getProtocolEndpoint().getUri().toString(); + } + + public String getAbsoluteTestResourcePath(String path) { + return Paths.get("").resolve("src/test/resources").resolve(path).toAbsolutePath().toString(); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java new file mode 100644 index 000000000..c4397ec2f --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -0,0 +1,606 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetEditMetadataRequest; +import de.sovity.edc.client.gen.model.UiContractNegotiation; +import de.sovity.edc.client.gen.model.UiContractOffer; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiPolicyConstraint; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyLiteral; +import de.sovity.edc.client.gen.model.UiPolicyLiteralType; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import lombok.val; +import org.awaitility.Awaitility; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static de.sovity.edc.client.gen.model.ContractAgreementDirection.CONSUMING; +import static de.sovity.edc.client.gen.model.ContractAgreementDirection.PROVIDING; +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + + +class UiApiWrapperTest { + + private static final String PROVIDER_PARTICIPANT_ID = "provider"; + private static final String CONSUMER_PARTICIPANT_ID = "consumer"; + + @RegisterExtension + static EdcExtension providerEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + + private EdcClient providerClient; + private EdcClient consumerClient; + private MockDataAddressRemote dataAddress; + + @BeforeEach + void setup() { + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + providerEdcContext.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + consumerEdcContext.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) + dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + } + + @DisabledOnGithub + @Test + void provide_consume_assetMapping_policyMapping_agreements() { + // arrange + var data = "expected data 123"; + var yesterday = OffsetDateTime.now().minusDays(1); + + var constraintRequest = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(yesterday.toString()) + .build()) + .build(); + + var policyId = providerClient.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() + .policyDefinitionId("policy-1") + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(constraintRequest)) + .build()) + .build()).getId(); + + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("asset-1") + .title("AssetName") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .sovereignLegalName("my-sovereign") + .geoLocation("my-geolocation") + .nutsLocations(Arrays.asList("my-nuts-location1", "my-nuts-location2")) + .dataSampleUrls(Arrays.asList("my-data-sample-urls1", "my-data-sample-urls2")) + .referenceFileUrls(Arrays.asList("my-reference-files1", "my-reference-files2")) + .referenceFilesDescription("my-additional-description") + .conditionsForUse("my-conditions-for-use") + .dataUpdateFrequency("my-data-update-frequency") + .temporalCoverageFrom(LocalDate.parse("2007-12-03")) + .temporalCoverageToInclusive(LocalDate.parse("2024-01-22")) + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) + )) + .customJsonAsString(""" + {"test": "value"} + """) + .customJsonLdAsString(""" + {"https://public/some#key": "public LD value"} + """) + .privateCustomJsonAsString(""" + {"private_test": "private value"} + """) + .privateCustomJsonLdAsString(""" + {"https://private/some#key": "private LD value"} + """) + .build()).getId(); + assertThat(assetId).isEqualTo("asset-1"); + + providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId("cd-1") + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId) + .build()) + .build())) + .build()); + + var assets = providerClient.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + var asset = assets.get(0); + + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + assertThat(dataOffers).hasSize(1); + var dataOffer = dataOffers.get(0); + assertThat(dataOffer.getContractOffers()).hasSize(1); + var contractOffer = dataOffer.getContractOffers().get(0); + + // act + var negotiation = negotiate(dataOffer, contractOffer); + initiateTransfer(negotiation); + var providerAgreements = providerClient.uiApi().getContractAgreementPage().getContractAgreements(); + var consumerAgreements = consumerClient.uiApi().getContractAgreementPage().getContractAgreements(); + + // assert + assertThat(dataOffer.getEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); + assertThat(dataOffer.getParticipantId()).isEqualTo(PROVIDER_PARTICIPANT_ID); + assertThat(dataOffer.getAsset().getAssetId()).isEqualTo(assetId); + assertThat(dataOffer.getAsset().getTitle()).isEqualTo("AssetName"); + assertThat(dataOffer.getAsset().getConnectorEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); + assertThat(dataOffer.getAsset().getParticipantId()).isEqualTo(providerConnector.getParticipantId()); + assertThat(dataOffer.getAsset().getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); + assertThat(dataOffer.getAsset().getDescription()).isEqualTo("AssetDescription"); + assertThat(dataOffer.getAsset().getVersion()).isEqualTo("1.0.0"); + assertThat(dataOffer.getAsset().getLanguage()).isEqualTo("en"); + assertThat(dataOffer.getAsset().getMediaType()).isEqualTo("application/json"); + assertThat(dataOffer.getAsset().getDataCategory()).isEqualTo("dataCategory"); + assertThat(dataOffer.getAsset().getDataSubcategory()).isEqualTo("dataSubcategory"); + assertThat(dataOffer.getAsset().getDataModel()).isEqualTo("dataModel"); + assertThat(dataOffer.getAsset().getGeoReferenceMethod()).isEqualTo("geoReferenceMethod"); + assertThat(dataOffer.getAsset().getTransportMode()).isEqualTo("transportMode"); + assertThat(dataOffer.getAsset().getSovereignLegalName()).isEqualTo("my-sovereign"); + assertThat(dataOffer.getAsset().getGeoLocation()).isEqualTo("my-geolocation"); + assertThat(dataOffer.getAsset().getNutsLocations()).isEqualTo(Arrays.asList("my-nuts-location1", "my-nuts-location2")); + assertThat(dataOffer.getAsset().getDataSampleUrls()).isEqualTo(Arrays.asList("my-data-sample-urls1", "my-data-sample-urls2")); + assertThat(dataOffer.getAsset().getReferenceFileUrls()).isEqualTo(Arrays.asList("my-reference-files1", "my-reference-files2")); + assertThat(dataOffer.getAsset().getReferenceFilesDescription()).isEqualTo("my-additional-description"); + assertThat(dataOffer.getAsset().getConditionsForUse()).isEqualTo("my-conditions-for-use"); + assertThat(dataOffer.getAsset().getDataUpdateFrequency()).isEqualTo("my-data-update-frequency"); + assertThat(dataOffer.getAsset().getTemporalCoverageFrom()).isEqualTo(LocalDate.parse("2007-12-03")); + assertThat(dataOffer.getAsset().getTemporalCoverageToInclusive()).isEqualTo(LocalDate.parse("2024-01-22")); + assertThat(dataOffer.getAsset().getLicenseUrl()).isEqualTo("https://license-url"); + assertThat(dataOffer.getAsset().getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); + assertThat(dataOffer.getAsset().getCreatorOrganizationName()).isEqualTo("Curator Name provider"); + assertThat(dataOffer.getAsset().getPublisherHomepage()).isEqualTo("publisherHomepage"); + assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyMethod()).isFalse(); + assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyPath()).isFalse(); + assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyQueryParams()).isFalse(); + assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyBody()).isFalse(); + assertThatJson(dataOffer.getAsset().getCustomJsonAsString()).isEqualTo(""" + {"test": "value"} + """); + assertThatJson(dataOffer.getAsset().getCustomJsonLdAsString()).isEqualTo(""" + {"https://public/some#key":"public LD value"} + """); + assertThat(dataOffer.getAsset().getPrivateCustomJsonAsString()).isNullOrEmpty(); + assertThatJson(dataOffer.getAsset().getPrivateCustomJsonLdAsString()).isObject().isEmpty(); + + // while the data offer on the consumer side won't contain private properties, the asset page on the provider side should + assertThat(asset.getAssetId()).isEqualTo(assetId); + assertThat(asset.getTitle()).isEqualTo("AssetName"); + assertThat(asset.getConnectorEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); + assertThat(asset.getParticipantId()).isEqualTo(providerConnector.getParticipantId()); + + assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" + { "test": "value" } + """); + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { "https://public/some#key": "public LD value" } + """); + assertThatJson(asset.getPrivateCustomJsonAsString()).isEqualTo(""" + { "private_test": "private value" } + """); + assertThatJson(asset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { "https://private/some#key": "private LD value" } + """); + + // Contract Agreement + assertThat(providerAgreements).hasSize(1); + var providerAgreement = providerAgreements.get(0); + + assertThat(consumerAgreements).hasSize(1); + var consumerAgreement = consumerAgreements.get(0); + + assertThat(providerAgreement.getContractAgreementId()).isEqualTo(consumerAgreement.getContractAgreementId()); + + // Provider Contract Agreement + assertThat(providerAgreement.getContractAgreementId()).isEqualTo(negotiation.getContractAgreementId()); + assertThat(providerAgreement.getDirection()).isEqualTo(PROVIDING); + assertThat(providerAgreement.getCounterPartyAddress()).isEqualTo("http://localhost:23003/api/dsp"); + assertThat(providerAgreement.getCounterPartyId()).isEqualTo(CONSUMER_PARTICIPANT_ID); + + assertThat(providerAgreement.getAsset().getAssetId()).isEqualTo(assetId); + var providingContractPolicyConstraint = providerAgreement.getContractPolicy().getConstraints().get(0); + assertThat(providingContractPolicyConstraint).usingRecursiveComparison().isEqualTo(providingContractPolicyConstraint); + + assertThat(providerAgreement.getAsset().getAssetId()).isEqualTo(assetId); + assertThat(providerAgreement.getAsset().getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); + assertThat(providerAgreement.getAsset().getTitle()).isEqualTo("AssetName"); + assertThat(providerAgreement.getAsset().getDescription()).isEqualTo("AssetDescription"); + + // Consumer Contract Agreement + assertThat(consumerAgreement.getContractAgreementId()).isEqualTo(negotiation.getContractAgreementId()); + assertThat(consumerAgreement.getDirection()).isEqualTo(CONSUMING); + assertThat(consumerAgreement.getCounterPartyAddress()).isEqualTo(dataOffer.getEndpoint()); + assertThat(consumerAgreement.getCounterPartyId()).isEqualTo(PROVIDER_PARTICIPANT_ID); + assertThat(consumerAgreement.getAsset().getAssetId()).isEqualTo(assetId); + + var consumingContractPolicyConstraint = consumerAgreement.getContractPolicy().getConstraints().get(0); + assertThat(consumingContractPolicyConstraint).usingRecursiveComparison().isEqualTo(consumingContractPolicyConstraint); + + assertThat(consumerAgreement.getAsset().getAssetId()).isEqualTo(assetId); + assertThat(consumerAgreement.getAsset().getTitle()).isEqualTo(assetId); + + // Test Policy + assertThat(contractOffer.getPolicy().getConstraints()).hasSize(1); + var constraint = contractOffer.getPolicy().getConstraints().get(0); + assertThat(constraint.getLeft()).isEqualTo("POLICY_EVALUATION_TIME"); + assertThat(constraint.getOperator()).isEqualTo(OperatorDto.GT); + assertThat(constraint.getRight().getType()).isEqualTo(UiPolicyLiteralType.STRING); + assertThat(constraint.getRight().getValue()).isEqualTo(yesterday.toString()); + + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); + + validateTransferProcessesOk(); + } + + @Test + void canOverrideTheWellKnowPropertiesUsingTheCustomProperties() { + // arrange + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("asset-1") + .title("will be overridden") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, "http://example.com/base" + )) + .customJsonLdAsString(""" + { + "http://purl.org/dc/terms/title": "The real title", + "http://purl.org/dc/terms/spatial": { + "http://purl.org/dc/terms/identifier": ["a", "b", "c"] + }, + "http://example.com/an-actual-custom-property": "custom value" + } + """) + .build()).getId(); + assertThat(assetId).isEqualTo("asset-1"); + + // act + val assets = providerClient.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + val asset = assets.get(0); + + // assert + + // while the data offer on the consumer side won't contain private properties, the asset page on the provider side should + assertThat(asset.getAssetId()).isEqualTo(assetId); + // overridden property + assertThat(asset.getTitle()).isEqualTo("The real title"); + // added property + assertThat(asset.getNutsLocations()).isEqualTo(List.of("a", "b", "c")); + // remaining custom property + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/an-actual-custom-property": "custom value" + } + """); + } + + // TODO throw an error if the id is overridden + + @DisabledOnGithub + @Test + void customTransferRequest() { + // arrange + var data = "expected data 123"; + + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("asset-1") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) + )) + .build()).getId(); + assertThat(assetId).isEqualTo("asset-1"); + + var policyId = providerClient.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() + .policyDefinitionId("policy-1") + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build()).getId(); + + providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId("cd-1") + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of()) + .build()); + + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + assertThat(dataOffers).hasSize(1); + var dataOffer = dataOffers.get(0); + assertThat(dataOffer.getContractOffers()).hasSize(1); + var contractOffer = dataOffer.getContractOffers().get(0); + + // act + var negotiation = negotiate(dataOffer, contractOffer); + var transferRequestJsonLd = Json.createObjectBuilder() + .add( + Prop.Edc.DATA_DESTINATION, + getDatasinkPropertiesJsonObject() + ) + .add(Prop.Edc.CTX + "transferType", Json.createObjectBuilder() + .add(Prop.Edc.CTX + "contentType", "application/octet-stream") + .add(Prop.Edc.CTX + "isFinite", true) + ) + .add(Prop.Edc.CTX + "protocol", HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .add(Prop.Edc.CTX + "managedResources", false) + .build(); + var transferRequest = InitiateCustomTransferRequest.builder() + .contractAgreementId(negotiation.getContractAgreementId()) + .transferProcessRequestJsonLd(JsonUtils.toJson(transferRequestJsonLd)) + .build(); + consumerClient.uiApi().initiateCustomTransfer(transferRequest); + + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); + } + + @DisabledOnGithub + @Test + void editAssetMetadataOnLiveContract() { + // arrange + var data = "expected data 123"; + + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("asset-1") + .title("Bad Asset Title") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) + )) + .customJsonAsString(""" + { + "test": "value" + } + """) + .customJsonLdAsString(""" + { + "test": "not a valid key, will be deleted", + "http://example.com/key-to-delete": "with a valida key", + "http://example.com/key-to-edit": "with a valida key" + } + """) + .privateCustomJsonAsString(""" + { + "private-test": "value" + } + """) + .privateCustomJsonLdAsString(""" + { + "private-test": "not a valid key, will be deleted", + "http://example.com/private-key-to-delete": "private with a valid key", + "http://example.com/private-key-to-edit": "private with a valid key" + } + """) + .build()).getId(); + + providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId("cd-1") + .accessPolicyId("always-true") + .contractPolicyId("always-true") + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId) + .build()) + .build())) + .build()); + + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + assertThat(dataOffers).hasSize(1); + var dataOffer = dataOffers.get(0); + assertThat(dataOffer.getContractOffers()).hasSize(1); + var contractOffer = dataOffer.getContractOffers().get(0); + var negotiation = negotiate(dataOffer, contractOffer); + + // act + providerClient.uiApi().editAssetMetadata(assetId, UiAssetEditMetadataRequest.builder() + .title("Good Asset Title") + .customJsonAsString(""" + { + "edited": "new value" + } + """) + .customJsonLdAsString(""" + { + "edited": "not a valid key, will be deleted", + "http://example.com/key-to-delete": null, + "http://example.com/key-to-edit": "with a valid key", + "http://example.com/extra": "value to add" + } + """) + .privateCustomJsonAsString(""" + { + "private-edited": "new value" + } + """) + .privateCustomJsonLdAsString(""" + { + "private-edited": "not a valid key, will be deleted", + "http://example.com/private-key-to-delete": null, + "http://example.com/private-key-to-edit": "private with a valid key", + "http://example.com/private-extra": "private value to add" + } + """) + .build()); + initiateTransfer(negotiation); + + // assert + assertThat(consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); + val firstAsset = providerClient.uiApi().getContractAgreementPage().getContractAgreements().get(0).getAsset(); + assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); + assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" + { + "edited": "new value" + } + """); + assertThatJson(firstAsset.getCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/key-to-edit": "with a valid key", + "http://example.com/extra": "value to add" + } + """); + assertThat(firstAsset.getPrivateCustomJsonAsString()).isEqualTo(""" + { + "private-edited": "new value" + } + """); + assertThatJson(firstAsset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/private-key-to-edit": "private with a valid key", + "http://example.com/private-extra": "private value to add" + } + """); + validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); + validateTransferProcessesOk(); + assertThat(providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0).getAssetName()).isEqualTo("Good Asset Title"); + } + + private UiContractNegotiation negotiate(UiDataOffer dataOffer, UiContractOffer contractOffer) { + var negotiationRequest = ContractNegotiationRequest.builder() + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); + + var negotiationId = consumerClient.uiApi().initiateContractNegotiation(negotiationRequest) + .getContractNegotiationId(); + + var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + ); + + assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + return negotiation; + } + + private void initiateTransfer(UiContractNegotiation negotiation) { + var contractAgreementId = negotiation.getContractAgreementId(); + var transferRequest = InitiateTransferRequest.builder() + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataAddress.getDataSinkProperties()) + .build(); + consumerClient.uiApi().initiateTransfer(transferRequest); + } + + private void validateTransferProcessesOk() { + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + var providing = providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); + var consuming = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); + assertThat(providing.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + assertThat(consuming.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + }); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private JsonObject getDatasinkPropertiesJsonObject() { + var props = dataAddress.getDataSinkProperties(); + return Json.createObjectBuilder((Map) (Map) props).build(); + } + + private String getProtocolEndpoint(ConnectorRemote connector) { + return connector.getConfig().getProtocolEndpoint().getUri().toString(); + } +} diff --git a/tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql b/tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql new file mode 100644 index 000000000..30929f8bd --- /dev/null +++ b/tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql @@ -0,0 +1,67 @@ +-- +-- Data for Name: edc_asset; Type: TABLE DATA; Schema: public; Owner: edc +-- + + + +-- +-- Data for Name: edc_asset_dataaddress; Type: TABLE DATA; Schema: public; Owner: edc +-- + + + +-- +-- Data for Name: edc_asset_property; Type: TABLE DATA; Schema: public; Owner: edc +-- + + + +-- +-- Data for Name: edc_contract_agreement; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_contract_agreement VALUES ('first-cd:28356d13-7fac-4540-80f2-22972c975ecb', 'urn:connector:provider', 'urn:connector:consumer', 1695207988, 1695207986, 1726743986, 'urn:artifact:first-asset:1.0', '{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}}'); + + +-- +-- Data for Name: edc_contract_definitions; Type: TABLE DATA; Schema: public; Owner: edc +-- + + + +-- +-- Data for Name: edc_contract_negotiation; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_contract_negotiation VALUES ('793d9064-8466-466e-93d1-25c379942c0d', NULL, 'urn:connector:example-provider', 'http://localhost:' || + '21003/api/v1/ids/data', 'ids-multipart', 0, 1200, 1, 1695207988404, NULL, 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '[{"id":"first-cd:95c164c0-2bdd-4c1e-82e2-bec36e0664a5","policy":{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}},"asset":{"id":"urn:artifact:first-asset:1.0","createdAt":1695207986324,"properties":{"asset:prop:id":"urn:artifact:first-asset:1.0"}},"provider":"urn:connector:provider","consumer":"urn:connector:consumer","offerStart":null,"offerEnd":null,"contractStart":"2023-09-20T11:06:26.32476749Z","contractEnd":"2024-09-19T11:06:26.32476749Z"}]', '{}', NULL, 1695207986331, 1695207988404); + + +-- +-- Data for Name: edc_data_plane_instance; Type: TABLE DATA; Schema: public; Owner: edc +-- + +-- +-- Data for Name: edc_lease; Type: TABLE DATA; Schema: public; Owner: edc +-- + + + +-- +-- Data for Name: edc_policydefinitions; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_policydefinitions VALUES ('always-true', '[{"edctype":"dataspaceconnector:permission","uid":null,"target":null,"action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"ALWAYS_TRUE"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"true"},"operator":"EQ"}],"duties":[]}]', '[]', '[]', '{}', NULL, NULL, NULL, NULL, '{"@policytype":"set"}', 1695137823418); + + +-- +-- Data for Name: edc_transfer_process; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_transfer_process VALUES ('946aadd4-d4bf-47e9-8aea-c2279070e839', 'CONSUMER', 800, 1, 1695208011094, 1695208008652, '{}', NULL, '{"definitions":[]}', NULL, NULL, '[]', NULL, 1695208011094, '{}'); + +-- +-- Data for Name: edc_data_request; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_data_request VALUES ('f9a60bc8-0cb5-4f30-8604-2e3b1d020541', '946aadd4-d4bf-47e9-8aea-c2279070e839', 'http://localhost:21003/api/v1/ids/data', 'ids-multipart', 'consumer', 'urn:artifact:first-asset:1.0', 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '{"properties":{"baseUrl":"https://webhook.site/6d5008a7-8c29-4e14-83c1-cc64f86d5398","method":"POST","type":"HttpData"}}', false, '{}', '{"contentType":"application/octet-stream","isFinite":true}', '946aadd4-d4bf-47e9-8aea-c2279070e839'); diff --git a/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql b/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql new file mode 100644 index 000000000..0d40dfbbd --- /dev/null +++ b/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql @@ -0,0 +1,292 @@ +-- +-- Data for Name: edc_asset; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_asset VALUES ('urn:artifact:first-asset:1.0', 1695207769374); +INSERT INTO public.edc_asset VALUES ('urn:artifact:second-asset', 1695207798635); + + +-- +-- Data for Name: edc_asset_dataaddress; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_asset_dataaddress VALUES ('urn:artifact:first-asset:1.0', '{"baseUrl":"http://localhost:23001/api/test-backend/data-source","method":"GET","queryParams":"data=first-asset-data","type":"HttpData"}'); +INSERT INTO public.edc_asset_dataaddress VALUES ('urn:artifact:second-asset', '{"baseUrl":"http://localhost:23001/api/test-backend/data-source","method":"GET","queryParams":"data=second-asset-data","type":"HttpData"}'); + + +-- +-- Data for Name: edc_asset_property; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:curatorOrganizationName', 'Example GmbH', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:originatorOrganization', 'Example GmbH', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#transportMode', 'Rail', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:contenttype', 'text/plain', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyMethod', 'false', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:version', '1.0', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#geoReferenceMethod', 'geo-ref', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:id', 'urn:artifact:first-asset:1.0', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#dataModel', 'data-model', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#dataSubcategory', 'Accidents', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyPath', 'false', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyQueryParams', 'false', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:language', 'https://w3id.org/idsa/code/EN', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:keywords', 'first, asset', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:name', 'First Asset', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:description', 'My First Asset', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyBody', 'false', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:endpointDocumentation', 'https://endpoint-documentation', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:publisher', 'https://publisher', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#dataCategory', 'Traffic Information', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:originator', 'http://localhost:21003/api/v1/ids/data', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:standardLicense', 'https://standard-license', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:usecase', 'my-use-case', 'java.lang.String'); +INSERT INTO public.edc_asset_property VALUES ('urn:artifact:second-asset', 'asset:prop:id', 'urn:artifact:second-asset', 'java.lang.String'); + + +-- +-- Data for Name: edc_contract_agreement; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_contract_agreement VALUES ('first-cd:28356d13-7fac-4540-80f2-22972c975ecb', 'urn:connector:provider', 'urn:connector:consumer', 1695207988, 1695207986, 1726743986, 'urn:artifact:first-asset:1.0', '{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}}'); + + +-- +-- Data for Name: edc_contract_definitions; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_contract_definitions VALUES ('first-cd', 'first-policy', 'first-policy', '{"criteria":[{"operandLeft":"asset:prop:id","operator":"in","operandRight":["urn:artifact:first-asset:1.0"]}]}', 1695207936442, 31536000); +INSERT INTO public.edc_contract_definitions VALUES ('second-cd', 'always-true', 'always-true', '{"criteria":[{"operandLeft":"asset:prop:id","operator":"in","operandRight":["urn:artifact:second-asset"]}]}', 1695207948854, 31536000); + + +-- +-- Data for Name: edc_contract_negotiation; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_contract_negotiation VALUES ('34ad04cd-4ce0-485f-a12e-aee0e37a9f03', '793d9064-8466-466e-93d1-25c379942c0d', 'urn:connector:example-connector', 'http://localhost:23003/api/v1/ids/data', 'ids-multipart', 1, 1200, 1, 1695207988502, NULL, 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '[{"id":"first-cd:95c164c0-2bdd-4c1e-82e2-bec36e0664a5","policy":{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}},"asset":{"id":"urn:artifact:first-asset:1.0","createdAt":1695207769374,"properties":{"asset:prop:curatorOrganizationName":"Example GmbH","http://w3id.org/mds#transportMode":"Rail","asset:prop:contenttype":"text/plain","asset:prop:datasource:http:hints:proxyMethod":"false","asset:prop:version":"1.0","http://w3id.org/mds#geoReferenceMethod":"geo-ref","asset:prop:id":"urn:artifact:first-asset:1.0","http://w3id.org/mds#dataModel":"data-model","http://w3id.org/mds#dataSubcategory":"Accidents","asset:prop:datasource:http:hints:proxyPath":"false","asset:prop:datasource:http:hints:proxyQueryParams":"false","asset:prop:language":"https://w3id.org/idsa/code/EN","asset:prop:keywords":"first, asset","asset:prop:name":"first-asset","asset:prop:description":"My First Asset","asset:prop:datasource:http:hints:proxyBody":"false","asset:prop:endpointDocumentation":"https://endpoint-documentation","asset:prop:publisher":"https://publisher","http://w3id.org/mds#dataCategory":"Traffic Information","asset:prop:originator":"http://localhost:21003/api/v1/ids/data","asset:prop:standardLicense":"https://standard-license"}},"provider":"urn:connector:provider","consumer":"urn:connector:consumer","offerStart":null,"offerEnd":null,"contractStart":"2023-09-20T11:06:26.324Z","contractEnd":"2024-09-19T11:06:26.324Z"}]', '{}', NULL, 1695207987357, 1695207988502); + +-- +-- Data for Name: edc_data_plane_instance; Type: TABLE DATA; Schema: public; Owner: edc +-- + +-- +-- Data for Name: edc_lease; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208010863, 60000, '70080388-d8d0-4a0f-b22d-6dec3f9ec10b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208071147, 60000, '1c6b5845-6d0a-481b-b7b3-9821cce8dbe4'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208131518, 60000, 'e919d926-86b6-4adf-82d2-000ff4680d4a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208191767, 60000, 'bd7b7d52-860c-4411-840f-7d30ccd6ea82'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208252016, 60000, 'a41aac77-9573-41c3-b320-67d4e1211548'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208312268, 60000, 'ae7a5a2a-7032-4032-8852-a33f1dfbf564'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208372514, 60000, '72dfc775-d08b-4953-a12c-5ed2b6cb9969'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208432761, 60000, 'b7c53380-3f48-472b-baeb-4cb4c8eaa8d4'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208492995, 60000, 'bd5f7e8d-598e-4ef5-9419-dbc2247fb4f1'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208553244, 60000, '19c3ad84-ca37-49c4-a6b0-86c92f7eb0c3'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208613485, 60000, '757191a2-956a-4057-b149-1ab567924b90'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208673732, 60000, '79d3685c-7d03-4353-b3b2-7c4ca88a6330'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208734001, 60000, 'fd82bbaf-88c4-4b64-96b6-d5ea100015c9'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208794234, 60000, '822c4fd9-c786-4849-a7cd-9ce63fb2d6ee'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208854497, 60000, 'c053655a-28e5-4eea-bd43-483e442cccfe'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208914783, 60000, 'f66cab9c-fbb6-421b-b1a8-42cede6af7e2'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695208975029, 60000, 'ddaeef38-2bc9-43cd-a083-1cdb97e0a85a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209035290, 60000, 'ad946d4d-0887-4b7a-9eb1-65c93aef5ec5'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209095539, 60000, '6333fcd4-a51d-4fcd-b449-13dfc348f295'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209155789, 60000, '6fc32793-0831-4e2c-b7ec-629834ec4b89'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209216047, 60000, '1f115d15-06be-4b69-b52f-e26ac4d1c923'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209276309, 60000, '978e21e8-ca45-4571-99e2-9e29e728a32c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209336562, 60000, '607f985e-e927-400f-8f8e-701661772c91'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209396829, 60000, '6e4a2083-8f49-4d38-a3a5-9652d8fee811'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209457069, 60000, '20acb8fe-79a2-4d63-b607-790de67ea918'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209517322, 60000, 'acb710ca-0600-4a30-b784-94530c155ce2'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209577577, 60000, 'aa0b2cdb-1bc7-43b7-8517-54c8ef1b5518'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209637822, 60000, '33475ffd-ab20-4064-80e0-03e710dbb7d5'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209698064, 60000, '88163bae-e57c-4863-89a9-5ce8862d412b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209758311, 60000, '363f7604-c2ea-4211-8bc6-5a35bc66119a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209818566, 60000, 'bb6ee8b3-7968-46c2-bf4a-b33b0b8aa623'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209878815, 60000, '6cd549c0-878f-4fe1-a365-b1621dbbb692'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209939072, 60000, 'd0d12685-1cfc-45ee-bb68-098c4870caff'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695209999345, 60000, 'c65e94e8-a2ba-436d-aaf1-c010a2bbf1d0'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210059598, 60000, '0114c5f9-0fb3-4e1d-bc14-1b3710ea831d'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210119851, 60000, 'e56fad21-5ca6-49dc-acb5-6c2a1cbab640'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210180119, 60000, 'dec31d6d-d0b7-433a-ae65-6abbf2a927ce'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210240372, 60000, '3a102ace-b301-4108-93f5-0a4650f459e1'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210300643, 60000, '477260f9-9285-44e0-af58-0fdcfb39f4e0'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210360926, 60000, '65abc39f-1d03-4f3d-8db4-32652a6df216'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210421168, 60000, 'a636d551-159a-40d7-82df-70e7b0c0ec14'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210481435, 60000, 'b8f86ea2-e7b6-4268-8c66-0be2bc5f7d75'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210541691, 60000, '95ec3530-6616-47bb-a0ce-01707d0ebd49'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210601976, 60000, '421d69df-8bda-4863-8ad5-f47593d28027'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210662248, 60000, 'e07dcbcd-36e4-4b83-a7d7-fa303ec4e5ca'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210722613, 60000, '446ba085-46f4-4f81-bdd1-6b37ce49ef45'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210782876, 60000, 'a0918ea0-8026-44d6-b04d-a1f1c65ff049'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210843144, 60000, '36f29613-de1e-4574-b12a-94a68747e1e5'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210903467, 60000, '358b182f-eca0-46f6-803c-1e7e0efc3f28'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695210963730, 60000, '497e1529-5b21-47bd-862b-02bf9e319dfa'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211023962, 60000, '0b137ded-6621-43e3-ad28-9fdae4e088bc'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211084241, 60000, '7b89cf3d-ee7b-46c4-a22d-0fc387585498'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211144486, 60000, 'da8f9b53-66bc-43d5-b12f-a02b70fa1812'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211204816, 60000, 'a0a521fc-22bc-4aae-91bf-49804e7cdcde'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211265137, 60000, '8eb86cfa-a831-4bd1-9848-1e032392b708'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211325446, 60000, 'f595da12-a76a-4a0b-810d-b3a4be024b8c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211385712, 60000, '6c9a3514-316a-4468-8ede-fbedc1d06ce5'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211446021, 60000, '9d48c8ae-17f3-4512-9007-8db2524f327f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211506293, 60000, '913daf55-c450-4d7b-9bf9-20948b77856e'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211566554, 60000, '60373891-24a4-4d4e-bbf3-492996c1374c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211626827, 60000, '96413c12-d4fc-417b-8455-a9efb841920a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211687553, 60000, '6e7b6ac2-17ae-4a02-b974-6da1c35c2250'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211748069, 60000, '2d5de73e-2c5c-4553-967f-18c8adcec211'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211808351, 60000, 'e67e3c12-f73a-4aed-8eef-45262b6b2f0a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211868621, 60000, '1aad7ebc-5b0f-4020-a3f1-9bab43bd130f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211928886, 60000, 'f955859d-f8b1-4d00-8330-10f8347452d7'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695211989171, 60000, '4c42e375-02e4-40aa-b4cc-671f20006a59'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212049439, 60000, '8a52fd3b-8c5b-4386-addc-7f32fa496f94'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212109700, 60000, '924c4be8-abd2-46d9-ba2e-91524809e686'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212169966, 60000, 'bbf29d09-33ae-4f6b-be1d-a47e0e5e0737'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212230241, 60000, '5aa44045-3642-4e5c-9711-4841b5681d92'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212290503, 60000, '470c4b7e-bfc9-4406-b275-f630c7350d33'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212350768, 60000, '09946130-38bc-44ee-b3d1-90e4323e065c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212411055, 60000, 'f0418e24-716a-4398-a5aa-1601a1b23bfb'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212471315, 60000, 'eeb470cf-4d46-48e4-8c00-f7f4e86e83c3'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212531573, 60000, 'f4d34f46-c97d-4b52-8449-841e4b1238bf'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212591829, 60000, 'be23a391-731f-4a7b-abd5-836e4f681747'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212652111, 60000, '2793ebaa-62c6-40f6-b260-a39e9d4eac76'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212712387, 60000, 'e17b5904-eba7-4cad-b71a-b541068f745f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212772645, 60000, 'f51d2efa-dd23-4176-adeb-8d740b72bc41'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212832905, 60000, '8eb68ed4-3edd-4c0f-bbff-a9731e31413f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212893185, 60000, '61875057-cd7b-47a1-9dbf-e0ade78ba62b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695212953474, 60000, '474b6909-7cb6-4be0-835b-4d7e456b9ea1'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213013731, 60000, '9ad2c665-a6ac-45a1-aee9-a289dcd1fb73'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213073994, 60000, '241d0ca1-fd32-4da4-bd3b-f29b46f977e7'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213134260, 60000, '8f13d5d5-3538-461b-9292-cc34fc17c774'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213194524, 60000, '5fa04667-c51d-409c-9461-9b5491e3349a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213254806, 60000, '92724334-01e5-4d83-8f58-8879b58433c7'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213315078, 60000, 'ec6b15c0-7fbf-4356-9dde-826f043aa7db'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213375377, 60000, '0d15ed6c-2172-4494-b9d0-49531733cfec'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213435657, 60000, 'bcfd1597-2224-45a3-9639-97cc5e93cfb2'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213495926, 60000, '644f1bbc-92d2-4821-943e-47f7d99ec35d'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213556200, 60000, '58f25b8f-1846-4187-818d-a68b86a6d720'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213616482, 60000, '0fb4ad31-06f7-41c2-8c67-3408a6ac77fb'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213676758, 60000, '79cdb279-35fd-4964-803b-62149549cb9c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213737020, 60000, '1e6b0aef-1df2-49c3-a5f8-78c53ae020aa'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213797290, 60000, '3ed0412b-038e-458c-ab90-98fa40afbe4d'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213857560, 60000, '831cf5f4-4140-456c-a14e-83e622a736ee'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213917822, 60000, '430b44e1-1712-4925-8ae9-87fddbeb412e'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695213978116, 60000, '764c7a26-82f3-456a-8a1c-cb1edb0d2207'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214038445, 60000, '18bb2a60-ac4e-4f5d-87e4-58e41759a4e1'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214098714, 60000, 'cf3a2416-f917-4af2-a5d5-d2c395861f76'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214158968, 60000, '1fba22ed-e697-492b-8f53-8b1617804248'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214219214, 60000, '51f5d170-f5ba-48a6-925b-606596fb15ca'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214279464, 60000, 'c509e43c-9103-4de0-a01c-bc529494002c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214339729, 60000, '083f74a1-8e9d-44f3-91b0-75c8c53afacd'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214399970, 60000, '3dbabac2-432e-42be-9cce-d2762d4eef50'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214460219, 60000, '4608d517-b691-474e-94e6-ec0102f839b6'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214520473, 60000, '030422b6-6574-40da-a672-d41d6d7d701e'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214580726, 60000, '11d37a34-ef21-4698-8639-2a4dab72553a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214640977, 60000, '97b0700f-00d0-443e-a9e5-0a855333e601'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214701227, 60000, '08115fc1-8714-41da-87c9-439d0b2af0b1'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214761493, 60000, 'f063fe96-d0c6-49d5-ad75-71da3b475849'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214821758, 60000, '72c944e7-9953-4a02-be26-6f95bc3fa357'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214882003, 60000, 'c8b0f0c3-62f5-4ee1-9ef3-670703be5bd6'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695214942297, 60000, 'a7d51b4b-fbfe-41ea-ac7a-b710a6d75897'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215002928, 60000, '0419b198-7165-4253-ba97-a20ebf2f96a8'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215063601, 60000, '86f773cd-0e43-430b-9105-f3641d464899'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215124190, 60000, 'c0171539-be72-479b-bb73-77b68649230b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215184722, 60000, '1276a425-cff9-4e04-ba9f-26511cad528b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215245255, 60000, '3b78d677-0a74-4f15-9d5c-18e662eb61da'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215305864, 60000, '1ce94378-a09d-4723-8644-8076e8cf8b8a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215366459, 60000, '142c2de3-c62a-4bd3-96ea-8f9799d1eb63'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215427090, 60000, '76eff2f3-66ff-4e75-83c7-10943ab8cc06'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215487690, 60000, '9990a113-6ceb-40e6-8a2a-48ecd0a04400'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215548308, 60000, '8ea71d36-68fc-4367-b08a-a836ea0532df'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215608921, 60000, '2ddb3989-965b-48f7-b090-e714e6bc4c3d'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215669522, 60000, 'f32194d1-13e2-4581-bba8-a5dd86a42004'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215730125, 60000, 'babb1962-faf0-44d1-9b91-34e1dfbe3677'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215790760, 60000, '829f65ca-f175-4f4b-b1aa-cd028c13e49f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215851394, 60000, 'd5ceeb0e-eaed-4c9b-b807-109c9b6547b1'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215912023, 60000, '5c5ea417-e788-4099-a699-19cd37b76945'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695215972644, 60000, '14e8a4f3-ab84-48b4-88e0-9549440fa95f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216033259, 60000, '765ecafb-44cc-4a7c-a5ce-828b4919729b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216093883, 60000, 'f0e33d91-6c6c-425b-a4b6-cba41590f029'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216154504, 60000, '87a59629-0dd7-4aec-bf80-2213de732711'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216215108, 60000, '850e8ab1-c76d-4c68-93ae-3b213cc7d031'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216275727, 60000, '2bcd7495-4da5-42d0-9a92-d9d32f755000'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216336373, 60000, 'de49c1f0-1e66-4980-98e4-23fd4c1e00cb'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216397016, 60000, 'b62d6656-6696-4bc6-ae71-4f95c66b8360'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216457640, 60000, '0389736b-b491-424f-bed2-671d4a96383c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216518281, 60000, '4c486f7f-ac6f-46f4-9672-c8ce5b132272'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216578911, 60000, '0142fa88-648e-4302-baf4-ba8fb081e291'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216639534, 60000, 'fb182422-9dce-4b36-9863-9b6673982b3c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216700144, 60000, '352ce55e-1393-45a7-8201-b0dee4c2541f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216760746, 60000, '8e3ea30f-7359-4c4c-ae68-fc01dbc39697'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216821373, 60000, '04a317fa-8b93-4052-afb3-5c46d9a9e44d'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216882029, 60000, '9ead3d6a-204d-4bb9-b45d-a359c9f7a8af'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695216942664, 60000, '873aa530-8524-453c-af2f-282c6e67a7f9'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217003279, 60000, 'b92a3d76-24b7-4efb-b563-6709306176b3'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217063910, 60000, '2313bf50-5f64-4c45-8ba5-20041a30ddeb'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217124536, 60000, '4c4a3a55-0786-4abf-b7a9-97d3240b93e8'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217185157, 60000, '978a0783-a979-47d2-bf16-86795e32a1b9'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217245780, 60000, 'cf3a877b-79f9-4ab8-9030-8c965d750cba'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217306429, 60000, 'f30ebf08-fa35-4f26-95b4-1c9b1eecc3c9'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217367085, 60000, 'bd53c6e9-0eee-4fe6-b833-ddaafe6098c3'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217427699, 60000, 'f4fee769-85c5-4db1-a204-c8db6e0f916f'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217488306, 60000, '3a118c7e-a1ed-4cef-bc26-af7a837a3b25'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217548918, 60000, '87107235-4a45-4811-9b03-3f8f8b2788cb'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217609620, 60000, 'ce853721-a41c-4e28-a287-7a7f9541857b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217670247, 60000, '4c327909-ecb3-49ad-9b43-7e283e694dec'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217730850, 60000, 'adc0bb07-2664-4493-9767-f52ccfc3e4a6'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217791252, 60000, '3f417fe6-2854-45b9-935f-7de79b64c1bf'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217851511, 60000, 'd66dfda4-aaef-48dc-a1ec-73ceee38ab26'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217911769, 60000, '2e5431bc-40be-4c0a-98df-c43d85e828a2'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695217972022, 60000, '53d8bfa4-e204-412e-b715-9eeed541e1b7'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218032277, 60000, '8e0dc6af-e2b8-4f74-9426-3f52428edabf'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218092532, 60000, 'e99c185e-2f8d-4515-9b3f-fdf5b7211ea5'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218152828, 60000, '52993024-bf3d-4093-897b-c9995a8ffcb6'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218213088, 60000, 'e6e57f51-7bc9-46ab-a1f2-864c2fd73404'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218273333, 60000, 'cb785654-4760-4167-942c-eee463572240'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218333581, 60000, '628b6548-5287-4a33-8b65-53af36c8a153'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218393824, 60000, '93f8f29e-449a-4d51-afa0-10cecfb6c06e'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218454073, 60000, 'd13e0440-b278-40ba-b49f-414476e68340'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218514355, 60000, '289deb73-d591-412f-b478-e2d9d9b9484c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218574641, 60000, '46e7406a-272e-4673-974c-456e5b2cf8ca'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218634926, 60000, '8aafe444-64b0-4191-b44f-8732737bcdda'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218695204, 60000, '832db41b-5574-473b-8165-bbe92bff7d2a'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218755490, 60000, '61289f09-28c1-4078-b418-4490f93a12a4'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218815763, 60000, '957e10b5-e721-41bd-a16c-a9fa37adc965'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218876047, 60000, 'c403947a-b0ba-4e2b-a047-1cb20f5d8d92'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218936301, 60000, '07a6e41f-636b-4d03-b3e8-edf832dfa6f5'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695218996571, 60000, '2192254a-50f1-4bbb-ae78-62f8c4afe7be'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219056849, 60000, '020136b3-b44b-42e2-86db-c2f5659ea86e'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219117118, 60000, '2c1dd02e-3fa9-4079-abdc-db624332a0a9'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219177408, 60000, 'ccb3959e-fe5c-443c-8bde-d3dbe4b9fab3'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219237684, 60000, 'fdf217ac-a3a9-4103-a2c9-396c44bea4ca'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219297943, 60000, 'e3d73b2c-b259-4704-b75f-4f634280f224'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219358204, 60000, 'de9b2b16-fb62-442b-a749-318660449f5c'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219418458, 60000, '3c32b9fc-41c3-4542-8b64-81ee15d98738'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219478721, 60000, 'cefe9a7e-bf4d-4c35-9c76-e0cbdc336689'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219538990, 60000, '82457869-79d2-4772-b364-1141a6ef984b'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219599252, 60000, 'ba1e9e62-e3db-41c7-9af6-590dd27e7182'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219659526, 60000, '037352ac-030e-4a69-91a7-3dfcf0f7e433'); +INSERT INTO public.edc_lease VALUES ('example-connector', 1695219719803, 60000, '34ab855d-4f66-4b9f-95f7-a79cd9f10bf0'); + + +-- +-- Data for Name: edc_policydefinitions; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_policydefinitions VALUES ('always-true', '[{"edctype":"dataspaceconnector:permission","uid":null,"target":null,"action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"ALWAYS_TRUE"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"true"},"operator":"EQ"}],"duties":[]}]', '[]', '[]', '{}', NULL, NULL, NULL, NULL, '{"@policytype":"set"}', 1695137823306); +INSERT INTO public.edc_policydefinitions VALUES ('first-policy', '[{"edctype":"dataspaceconnector:permission","uid":null,"target":null,"action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}]', '[]', '[]', '{}', NULL, NULL, NULL, NULL, '{"@policytype":"set"}', 1695207922457); + + +-- +-- Data for Name: edc_transfer_process; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_transfer_process VALUES ('27075fc4-b18f-44e1-8bde-a9f62817dab2', 'PROVIDER', 600, 1, 1695208010855, 1695208010083, '{}', NULL, '{"definitions":[]}', NULL, '{"properties":{"baseUrl":"http://localhost:23001/api/test-backend/data-source","method":"GET","queryParams":"data=first-asset-data","type":"HttpData"}}', '[]', '34ab855d-4f66-4b9f-95f7-a79cd9f10bf0', 1695208010855, '{}'); + + +-- +-- Data for Name: edc_data_request; Type: TABLE DATA; Schema: public; Owner: edc +-- + +INSERT INTO public.edc_data_request VALUES ('f9a60bc8-0cb5-4f30-8604-2e3b1d020541', '27075fc4-b18f-44e1-8bde-a9f62817dab2', 'http://localhost:23003/api/v1/ids/data', 'ids-multipart', 'urn:connector:example-connector', 'urn:artifact:first-asset:1.0', 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '{"properties":{"baseUrl":"https://webhook.site/6d5008a7-8c29-4e14-83c1-cc64f86d5398","method":"POST","type":"HttpData"}}', true, '{}', '{"contentType":"application/octet-stream","isFinite":true}', '27075fc4-b18f-44e1-8bde-a9f62817dab2'); diff --git a/utils/catalog-parser/README.md b/utils/catalog-parser/README.md new file mode 100644 index 000000000..67b1dc5f6 --- /dev/null +++ b/utils/catalog-parser/README.md @@ -0,0 +1,31 @@ + +
      +
      + + Logo + + +

      EDC-Connector Utilities:
      Catalog Fetcher / Catalog Parser

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this module + +A service for parsing DSP / DCAT Catalog Responses. + +## Why does this module exist? + +The Core EDC CatalogService returns a `byte[]`, and we need the parsing logic in multiple units. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/utils/catalog-parser/build.gradle.kts b/utils/catalog-parser/build.gradle.kts new file mode 100644 index 000000000..cdd99b1e4 --- /dev/null +++ b/utils/catalog-parser/build.gradle.kts @@ -0,0 +1,49 @@ +val lombokVersion: String by project + +val edcGroup: String by project +val edcVersion: String by project +val assertj: String by project +val mockitoVersion: String by project +val jakartaJsonVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api("org.glassfish:jakarta.json:${jakartaJsonVersion}") + api("${edcGroup}:core-spi:${edcVersion}") + api("${edcGroup}:control-plane-spi:${edcVersion}") + api("${edcGroup}:json-ld:${edcVersion}") + + implementation(project(":utils:json-and-jsonld-utils")) + + implementation("org.apache.commons:commons-lang3:3.13.0") + implementation("org.apache.commons:commons-collections4:4.4") + implementation("commons-io:commons-io:2.13.0") + + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testImplementation(project(":utils:test-utils")) + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.mockito:mockito-inline:${mockitoVersion}") + testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java new file mode 100644 index 000000000..a31d5461a --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.catalog; + +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; +import de.sovity.edc.utils.catalog.model.DspCatalog; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.nio.charset.StandardCharsets; + +@RequiredArgsConstructor +public class DspCatalogService { + private final CatalogService catalogService; + private final DspDataOfferBuilder dspDataOfferBuilder; + + public DspCatalog fetchDataOffers(String endpoint) throws DspCatalogServiceException { + var catalogJson = fetchDcatResponse(endpoint); + return dspDataOfferBuilder.buildDataOffers(endpoint, catalogJson); + } + + private JsonObject fetchDcatResponse(String connectorEndpoint) { + var raw = fetchDcatRaw(connectorEndpoint); + var string = new String(raw, StandardCharsets.UTF_8); + return JsonUtils.parseJsonObj(string); + } + + @SneakyThrows + private byte[] fetchDcatRaw(String connectorEndpoint) { + return catalogService + .requestCatalog(connectorEndpoint, "dataspace-protocol-http", QuerySpec.max()) + .get() + .orElseThrow(DspCatalogServiceException::ofFailure); + } +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java new file mode 100644 index 000000000..1d2a53dc0 --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.catalog; + +import org.eclipse.edc.spi.result.Failure; + +public class DspCatalogServiceException extends RuntimeException { + public DspCatalogServiceException(String message) { + super(message); + } + + public static DspCatalogServiceException ofFailure(Failure failure) { + return new DspCatalogServiceException(failure.getFailureDetail()); + } +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java new file mode 100644 index 000000000..53ad296ff --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java @@ -0,0 +1,78 @@ +package de.sovity.edc.utils.catalog.mapper; + +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import lombok.val; +import org.eclipse.edc.connector.contract.spi.ContractId; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class DspContractOfferUtils { + + /** + * /!\ Workaround + *

      + * The Eclipse EDC uses a new random UUID for each policy that it returns and in turn a new contract ID. + * This Eclipse ID can't be used as such. + * As a workaround, we must introduce our own ID. + * For a first iteration, we will assume that the content of the policy remains the same (same content, same order) + * and hash it to use it as a key. + * + * @param contract The contract to compute an ID from + * @return A base64 string that can be used as an id for the {@code contract} + */ + public static String buildStableId(JsonObject contract) { + // NOTE: This doesn't enforce any property order and may cause trouble if the returned policy schema is not consistent. + // Use canonical form if needed later. + val noId = Json.createObjectBuilder(contract).remove(Prop.ID).build(); + val policyId = hash(noId); + + val currentId = ContractId.parseId(JsonLdUtils.string(contract, Prop.ID)) + .orElseThrow((failure) -> { + throw new RuntimeException("Failed to parse the contract id: " + failure.getFailureDetail()); + }); + + return currentId.definitionPart() + ":" + currentId.assetIdPart() + ":" + policyId; + } + + @NotNull + private static String hash(JsonObject noId) { + val policyJsonString = JsonUtils.toJson(noId); + val sha1 = sha1(policyJsonString); + // encoding with base16 to make the hash readable to humans (similarly to how the random UUID would have been readable) + val base16 = toBase16(sha1); + return toBase64(base16); + } + + @NotNull + private static String toBase64(String string) { + byte[] stringBytes = string.getBytes(StandardCharsets.UTF_8); + byte[] bytes = Base64.getEncoder().encode(stringBytes); + return new String(bytes); + } + + @NotNull + private static String toBase16(byte[] bytes) { + val sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(Character.forDigit(b >> 4 & 0xf, 16)); + sb.append(Character.forDigit(b & 0xf, 16)); + } + return sb.toString(); + } + + private static byte[] sha1(String string) { + try { + return MessageDigest.getInstance("sha-1").digest(string.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java new file mode 100644 index 000000000..02c0ec338 --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.catalog.mapper; + +import de.sovity.edc.utils.catalog.DspCatalogServiceException; +import de.sovity.edc.utils.catalog.model.DspCatalog; +import de.sovity.edc.utils.catalog.model.DspContractOffer; +import de.sovity.edc.utils.catalog.model.DspDataOffer; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor +public class DspDataOfferBuilder { + + private final JsonLd jsonLd; + + public DspCatalog buildDataOffers(String endpoint, JsonObject json) { + json = jsonLd.expand(json).orElseThrow(DspCatalogServiceException::ofFailure); + String participantId = JsonLdUtils.string(json, Prop.Edc.PARTICIPANT_ID); + + return new DspCatalog( + endpoint, + participantId, + JsonLdUtils.listOfObjects(json, Prop.Dcat.DATASET).stream() + .map(this::buildDataOffer) + .toList() + ); + } + + private DspDataOffer buildDataOffer(JsonObject dataset) { + var contractOffers = JsonLdUtils.listOfObjects(dataset, Prop.Odrl.HAS_POLICY).stream() + .map(this::buildContractOffer) + .toList(); + + var distributions = JsonLdUtils.listOfObjects(dataset, Prop.Dcat.DISTRIBUTION_AS_USED_BY_CORE_EDC); + + var assetProperties = Json.createObjectBuilder(dataset) + .remove(Prop.TYPE) + .remove(Prop.Odrl.HAS_POLICY) + .remove(Prop.Dcat.DISTRIBUTION_AS_USED_BY_CORE_EDC) + .build(); + + + return new DspDataOffer( + assetProperties, + contractOffers, + distributions + ); + } + + @NotNull + private DspContractOffer buildContractOffer(JsonObject json) { + return new DspContractOffer(JsonLdUtils.id(json), json); + } +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java new file mode 100644 index 000000000..e68bb07c5 --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.catalog.model; + +import lombok.Data; + +import java.util.List; + +@Data +public class DspCatalog { + private final String endpoint; + private final String participantId; + private final List dataOffers; +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java new file mode 100644 index 000000000..a5150fc00 --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.catalog.model; + +import jakarta.json.JsonObject; +import lombok.Data; + +@Data +public class DspContractOffer { + private final String contractOfferId; + private final JsonObject policyJsonLd; +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java new file mode 100644 index 000000000..278098e35 --- /dev/null +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.catalog.model; + +import jakarta.json.JsonObject; +import lombok.Data; + +import java.util.List; + +@Data +public class DspDataOffer { + private final JsonObject assetPropertiesJsonLd; + private final List contractOffers; + private final List distributions; +} diff --git a/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java b/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java new file mode 100644 index 000000000..88fa35cb2 --- /dev/null +++ b/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.utils.catalog; + +import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.response.StatusResult; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import static de.sovity.edc.utils.JsonUtils.toJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DspCatalogServiceTest { + String endpoint = "http://localhost:11003/api/v1/dsp"; + + private DspCatalogService newDspCatalogService(String resultJsonFilename) { + var catalogJson = readFile(resultJsonFilename); + var catalogService = mock(CatalogService.class); + + var result = CompletableFuture.completedFuture(StatusResult.success(catalogJson.getBytes(StandardCharsets.UTF_8))); + when(catalogService.requestCatalog(eq(endpoint), eq("dataspace-protocol-http"), eq(QuerySpec.max()))).thenReturn(result); + var monitor = mock(Monitor.class); + var dataOfferBuilder = new DspDataOfferBuilder(new TitaniumJsonLd(monitor)); + + return new DspCatalogService(catalogService, dataOfferBuilder); + } + + @Test + void testCatalogMapping() { + // arrange + var dspCatalogService = newDspCatalogService("catalogResponse.json"); + + // act + var actual = dspCatalogService.fetchDataOffers(endpoint); + + // assert + var offers = actual.getDataOffers(); + assertThat(offers).hasSize(1); + var offer = offers.get(0); + assertThat(actual.getEndpoint()).isEqualTo(endpoint); + assertThat(actual.getParticipantId()).isEqualTo("provider"); + assertThat(JsonLdUtils.id(offer.getAssetPropertiesJsonLd())) + .isEqualTo("test-1.0"); + assertThat(offer.getAssetPropertiesJsonLd().get(Prop.TYPE)).isNull(); + assertThat(JsonLdUtils.string(offer.getAssetPropertiesJsonLd(), Prop.Dcat.VERSION)).isEqualTo("1.0"); + + assertThat(offer.getContractOffers()).hasSize(1); + var co = offer.getContractOffers().get(0); + assertThat(co.getContractOfferId()).isEqualTo("policy-1"); + assertThat(toJson(co.getPolicyJsonLd())).contains("ALWAYS_TRUE"); + + assertThat(offer.getDistributions()).hasSize(1); + assertThat(JsonLdUtils.id(offer.getDistributions().get(0))).isEqualTo("dummy-distribution"); + } + + + @SneakyThrows + private String readFile(String fileName) { + var is = getClass().getResourceAsStream(fileName); + Objects.requireNonNull(is, "File not found: " + fileName); + return IOUtils.toString(is, StandardCharsets.UTF_8); + } +} diff --git a/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json b/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json new file mode 100644 index 000000000..d069f7302 --- /dev/null +++ b/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json @@ -0,0 +1,47 @@ +{ + "@id": "478a07bb-4df0-471e-aabe-c6d47558b329", + "@type": "dcat:Catalog", + "dcat:dataset": { + "@id": "test-1.0", + "@type": "dcat:Dataset", + "odrl:hasPolicy": { + "@id": "policy-1", + "@type": "odrl:Set", + "odrl:permission": { + "odrl:target": "test-1.0", + "odrl:action": { + "odrl:type": "USE" + }, + "odrl:constraint": [ + { + "odrl:leftOperand": "ALWAYS_TRUE", + "odrl:operator": { + "@id": "odrl:eq" + }, + "odrl:rightOperand": "true" + } + ] + }, + "odrl:prohibition": [], + "odrl:obligation": [], + "odrl:target": "test-1.0" + }, + "dcat:distribution": [{"@id": "dummy-distribution"}], + "http://www.w3.org/ns/dcat#version": "1.0", + "http://purl.org/dc/terms/title": "test" + }, + "dcat:service": { + "@id": "eea310e7-9725-4e77-b080-fe64ac5b6435", + "@type": "dcat:DataService", + "dct:terms": "connector", + "dct:endpointUrl": "http://localhost:12000/dsp" + }, + "edc:participantId": "provider", + "@context": { + "dct": "https://purl.org/dc/terms/", + "edc": "https://w3id.org/edc/v0.0.1/ns/", + "dcat": "https://www.w3.org/ns/dcat/", + "odrl": "http://www.w3.org/ns/odrl/2/", + "dspace": "https://w3id.org/dspace/v0.8/" + } +} diff --git a/utils/json-and-jsonld-utils/README.md b/utils/json-and-jsonld-utils/README.md new file mode 100644 index 000000000..66dacbbba --- /dev/null +++ b/utils/json-and-jsonld-utils/README.md @@ -0,0 +1,31 @@ + +
      +

      + + Logo + + +

      EDC-Connector Utilities:
      JSON / JSON-LD Utils

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this module + +JSON / JSON-LD Utilities + +## Why does this module exist? + +The JSON-LD mapping / utilities from this repository is re-used in other sovity repositories. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/utils/json-and-jsonld-utils/build.gradle.kts b/utils/json-and-jsonld-utils/build.gradle.kts new file mode 100644 index 000000000..fb5ae7d64 --- /dev/null +++ b/utils/json-and-jsonld-utils/build.gradle.kts @@ -0,0 +1,44 @@ +val lombokVersion: String by project + +val edcGroup: String by project +val edcVersion: String by project +val assertj: String by project +val mockitoVersion: String by project +val jakartaJsonVersion: String by project + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api("org.glassfish:jakarta.json:${jakartaJsonVersion}") + api("com.apicatalog:titanium-json-ld:1.3.2") + + implementation("org.apache.commons:commons-lang3:3.13.0") + implementation("org.apache.commons:commons-collections4:4.4") + implementation("commons-io:commons-io:2.13.0") + + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.mockito:mockito-inline:${mockitoVersion}") + testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") +} + +val sovityEdcGroup: String by project +group = sovityEdcGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java new file mode 100644 index 000000000..7ac01a187 --- /dev/null +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.StringReader; +import java.io.StringWriter; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonUtils { + + public static JsonObject parseJsonObj(String string) { + try (var reader = Json.createReader(new StringReader(string))) { + return reader.readObject(); + } + } + + public static JsonValue parseJsonValue(String string) { + try (var reader = Json.createReader(new StringReader(string))) { + return reader.readValue(); + } + } + + public static String toJson(JsonValue json) { + if (json == null) { + return "null"; + } + + var sw = new StringWriter(); + try (var writer = Json.createWriter(sw)) { + writer.write(json); + return sw.toString(); + } + } + +} diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java new file mode 100644 index 000000000..ec44ecdad --- /dev/null +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.jsonld; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.document.JsonDocument; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonLdUtils { + private static final JsonDocument EMPTY_CONTEXT_DOCUMENT = JsonDocument.of(Json.createObjectBuilder() + .add(Prop.CONTEXT, Json.createObjectBuilder()) + .build()); + + /** + * Compact JSON-LD, but don't compact property names to namespaces. + * + * @param json json-ld + * @return compacted values + */ + public static JsonObject tryCompact(JsonObject json) { + try { + return com.apicatalog.jsonld.JsonLd.compact(JsonDocument.of(json), EMPTY_CONTEXT_DOCUMENT).get(); + } catch (JsonLdError e) { + return json; + } + } + + /** + * Compact JSON-LD, but don't compact property names to namespaces. + * + * @param json json-ld + * @return compacted values + */ + public static JsonObject expandKeysOnly(JsonObject json) { + try { + var expanded = com.apicatalog.jsonld.JsonLd.expand(JsonDocument.of(json)).get(); + return com.apicatalog.jsonld.JsonLd.compact(JsonDocument.of(expanded), EMPTY_CONTEXT_DOCUMENT).get(); + } catch (JsonLdError e) { + return json; + } + } + + public static boolean isEmptyArray(JsonValue json) { + return list(json).isEmpty(); + } + + public static boolean isEmptyObject(JsonValue json) { + return object(json).isEmpty(); + } + + + /** + * Get the ID value of an object + * + * @param json json-ld + * @return id or null + */ + public static String id(JsonObject json) { + return string(json, "@id"); + } + + /** + * Get a string property + * + * @param json json-ld + * @return string value or null + */ + public static String string(JsonValue json) { + var value = value(json); + if (value == null) { + return null; + } + + return switch (value.getValueType()) { + case STRING -> ((JsonString) value).getString(); + case NUMBER -> ((JsonNumber) value).bigDecimalValue().toString(); + case FALSE -> "false"; + case TRUE -> "true"; + case NULL -> null; + // We do this over throwing errors because we want to be able to handle invalid json-ld + case ARRAY, OBJECT -> JsonUtils.toJson(value); + }; + } + + /** + * Get a offset date time property + * + * @param json json-ld + * @return offset date time value or null + */ + public static LocalDate localDate(JsonValue json) { + var str = string(json); + if (str == null) { + return null; + } + + try { + return LocalDate.parse(str); + } catch (DateTimeParseException e) { + return null; + } + } + + /** + * Get a boolean property + * + * @param json json-ld + * @return boolean value or null + */ + public static Boolean bool(JsonValue json) { + var value = value(json); + if (value == null) { + return null; + } + + return switch (value.getValueType()) { + case STRING -> switch (((JsonString) value).getString().toLowerCase()) { + case "true" -> Boolean.TRUE; + case "false" -> Boolean.FALSE; + default -> null; + }; + case FALSE -> Boolean.FALSE; + case TRUE -> Boolean.TRUE; + case NUMBER, NULL, ARRAY, OBJECT -> null; + }; + } + + /** + * Get a list property. + * + * @param json json-ld + * @return list of values + */ + public static List list(JsonValue json) { + return switch (json.getValueType()) { + case ARRAY -> json.asJsonArray(); + case FALSE, TRUE, NUMBER, STRING, OBJECT -> List.of(json); + case NULL -> List.of(); + }; + } + + /** + * Get a list property while unwrapping values and only keeping objects. + * + * @param json json-ld + * @return list of values + */ + public static List listOfObjects(JsonValue json) { + return list(json).stream() + .map(JsonLdUtils::value) // unwrap @value + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .toList(); + } + + /** + * Get the innermost @value of an object. Also removes wrappings in lists. + * + * @param json json-ld + * @return innermost @value + */ + public static JsonValue value(JsonValue json) { + return switch (json.getValueType()) { + case ARRAY -> { + var array = json.asJsonArray(); + if (array.isEmpty()) { + yield null; + } + yield value(array.get(0)); + } + case OBJECT -> { + var object = json.asJsonObject(); + if (object.containsKey("@value")) { + yield value(object.get("@value")); + } else { + yield object; + } + } + case STRING, NUMBER, FALSE, TRUE, NULL -> json; + }; + } + + /** + * Get a string property + * + * @param object json-ld + * @param key key + * @return string or null + */ + public static String string(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return null; + } + return string(field); + } + + /** + * Get a offset date time property + * + * @param object json-ld + * @param key key + * @return offset date time or null + */ + public static LocalDate localDate(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return null; + } + return localDate(field); + } + + /** + * Get an object property. Defaults to an empty object for ease of use if not found. + * + * @param object json-ld + * @param key key + * @return string or null + */ + public static JsonObject object(JsonObject object, String key) { + return object(object.get(key)); + } + + /** + * Get an object property. Defaults to an empty object for ease of use if not found. + * + * @param field json-ld + * @return string or null + */ + public static JsonObject object(JsonValue field) { + if (field == null) { + return JsonValue.EMPTY_JSON_OBJECT; + } + + var unwrapped = value(field); + if (unwrapped == null || unwrapped.getValueType() != JsonValue.ValueType.OBJECT) { + return JsonValue.EMPTY_JSON_OBJECT; + } + + return (JsonObject) unwrapped; + } + + /** + * Get a list property while unwrapping values and only keeping objects. + * + * @param object json-ld + * @param key key + * @return list of values + */ + public static List listOfObjects(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return List.of(); + } + return listOfObjects(field); + } + + /** + * Get a list of strings. defaults to empty list + * + * @param object json-ld + * @param key key + * @return string list or empty list + */ + public static List stringList(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return List.of(); + } + return list(field).stream() + .map(JsonLdUtils::string) + .toList(); + } + + /** + * Get a boolean property. defaults to null + * + * @param object json-ld + * @param key key + * @return boolean or null + */ + public static Boolean bool(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return null; + } + return bool(field); + } +} diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java new file mode 100644 index 000000000..b9f48c84c --- /dev/null +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.utils.jsonld.vocab; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Prop { + public final String ID = "@id"; + public final String TYPE = "@type"; + public final String VALUE = "@value"; + public final String CONTEXT = "@context"; + public final String LANGUAGE = "@language"; + public final String PROPERTIES = "properties"; + + @UtilityClass + public class Edc { + public final String CTX = "https://w3id.org/edc/v0.0.1/ns/"; + public final String CTX_ALIAS = "edc"; + public final String TYPE_ASSET = CTX + "Asset"; + public final String TYPE_DATA_ADDRESS = CTX + "DataAddress"; + public final String ID = CTX + "id"; + public final String PARTICIPANT_ID = CTX + "participantId"; + public final String PROPERTIES = CTX + "properties"; + public final String PRIVATE_PROPERTIES = CTX + "privateProperties"; + public final String DATA_ADDRESS = CTX + "dataAddress"; + public final String TYPE = CTX + "type"; + public final String BASE_URL = CTX + "baseUrl"; + public final String METHOD = CTX + "method"; + public final String PROXY_METHOD = CTX + "proxyMethod"; + public final String PROXY_PATH = CTX + "proxyPath"; + public final String PROXY_QUERY_PARAMS = CTX + "proxyQueryParams"; + public final String PROXY_BODY = CTX + "proxyBody"; + + // Transfer Request Related + public static String TYPE_TRANSFER_REQUEST = CTX + "TransferRequest"; + public final String CONNECTOR_ADDRESS = CTX + "connectorAddress"; + public final String CONTRACT_ID = CTX + "contractId"; + public final String CONNECTOR_ID = CTX + "connectorId"; + public final String ASSET_ID = CTX + "assetId"; + public final String DATA_DESTINATION = CTX + "dataDestination"; + public final String RECEIVER_HTTP_ENDPOINT = CTX + "receiverHttpEndpoint"; + } + + /** + * DCAT Vocabulary, see https://www.w3.org/TR/vocab-dcat-3 + */ + @UtilityClass + public class Dcat { + /** + * Context as specified in https://www.w3.org/TR/vocab-dcat-3/#normative-namespaces + */ + public final String CTX = "http://www.w3.org/ns/dcat#"; + + /** + * Context as used in the Core EDC, or atleast how its output from a DCAT request + */ + public final String CTX_WRONG_BUT_USED_BY_CORE_EDC = "https://www.w3.org/ns/dcat/"; + + public final String DATASET = CTX_WRONG_BUT_USED_BY_CORE_EDC + "dataset"; + public final String DISTRIBUTION = CTX + "distribution"; + public final String DISTRIBUTION_AS_USED_BY_CORE_EDC = CTX_WRONG_BUT_USED_BY_CORE_EDC + "distribution"; + public final String VERSION = CTX + "version"; + public final String KEYWORDS = CTX + "keyword"; + public final String LANDING_PAGE = CTX + "landingPage"; + public final String MEDIATYPE = CTX + "mediaType"; + public final String START_DATE = CTX + "startDate"; + public final String END_DATE = CTX + "endDate"; + public final String DOWNLOAD_URL = CTX + "downloadURL"; + } + + /** + * ODRL Vocabulary, see DCAT 3 Specification + */ + @UtilityClass + public class Odrl { + public final String CTX = "http://www.w3.org/ns/odrl/2/"; + public final String HAS_POLICY = CTX + "hasPolicy"; + } + + /** + * Dcterms Metadata Terms Vocabulary, see DCMI Metadata Terms + */ + @UtilityClass + public class Dcterms { + public final String CTX = "http://purl.org/dc/terms/"; + public final String IDENTIFIER = CTX + "identifier"; + public final String TITLE = CTX + "title"; + public final String DESCRIPTION = CTX + "description"; + public final String LANGUAGE = CTX + "language"; + public final String CREATOR = CTX + "creator"; + public final String PUBLISHER = CTX + "publisher"; + public final String LICENSE = CTX + "license"; + public final String TEMPORAL = CTX + "temporal"; + public final String ACCRUAL_PERIODICITY = CTX + "accrualPeriodicity"; + public final String SPATIAL = CTX + "spatial"; + public final String RIGHTS_HOLDER = CTX + "rightsHolder"; + public final String RIGHTS = CTX + "rights"; + public final String RIGHTS_STATEMENT = CTX + "RightsStatement"; + } + + /** + * Dcterms Metadata Terms Vocabulary, see DCAT 3 Specification + */ + @UtilityClass + public class SovityDcatExt { + public final String CTX = "https://semantic.sovity.io/dcat-ext#"; + public final String CUSTOM_JSON = CTX + "customJson"; + public final String PRIVATE_CUSTOM_JSON = CTX + "privateCustomJson"; + + @UtilityClass + public class HttpDatasourceHints { + public final String METHOD = CTX + "httpDatasourceHintsProxyMethod"; + public final String PATH = CTX + "httpDatasourceHintsProxyPath"; + public final String QUERY_PARAMS = CTX + "httpDatasourceHintsProxyQueryParams"; + public final String BODY = CTX + "httpDatasourceHintsProxyBody"; + } + } + + /** + * FOAF Vocabulary + */ + @UtilityClass + public class Foaf { + public final String CTX = "http://xmlns.com/foaf/0.1/"; + public final String ORGANIZATION = CTX + "Organization"; + public final String NAME = CTX + "name"; + public final String HOMEPAGE = CTX + "homepage"; + } + + /** + * Namespace mobilitydcatap as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class MobilityDcatAp { + public final String CTX = "https://w3id.org/mobilitydcat-ap/"; + public final String MOBILITY_THEME = CTX + "mobilityTheme"; + + @UtilityClass + public class DataCategoryProps { + public final String CTX = "https://w3id.org/mobilitydcat-ap/mobility-theme/"; + public final String DATA_CATEGORY = CTX + "data-content-category"; + public final String DATA_SUBCATEGORY = CTX + "data-content-sub-category"; + } + + public final String TRANSPORT_MODE = CTX + "transportMode"; + public final String GEO_REFERENCE_METHOD = CTX + "georeferencingMethod"; + public final String MOBILITY_DATA_STANDARD = CTX + "mobilityDataStandard"; + + // Optional property of mobilitydcatap:mobilityDataStandard + public final String SCHEMA = CTX + "schema"; + } + + /** + * Namespace skos as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class Skos { + public final String CTX = "http://www.w3.org/2004/02/skos/core#"; + public final String PREF_LABEL = CTX + "prefLabel"; + } + + /** + * Namespace adms as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class Adms { + public final String CTX = "http://www.w3.org/ns/adms#"; + public final String SAMPLE = CTX + "sample"; + } + + /** + * Namespace rdfs as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class Rdfs { + public final String CTX = "http://www.w3.org/2000/01/rdf-schema#"; + public final String LITERAL = CTX + "Literal"; + public final String LABEL = CTX + "label"; + } +} diff --git a/utils/test-connector-remote/README.md b/utils/test-connector-remote/README.md new file mode 100644 index 000000000..d830c70ce --- /dev/null +++ b/utils/test-connector-remote/README.md @@ -0,0 +1,31 @@ + +
      +
      + + Logo + + +

      Connector Remote (for Test Data)

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Utility + +Connector Remote for creating simple test data via the management API. + +## Why does this extension exist? + +So we can easily create fill test connectors with data for E2E testing. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/utils/test-connector-remote/build.gradle.kts b/utils/test-connector-remote/build.gradle.kts new file mode 100644 index 000000000..53563830f --- /dev/null +++ b/utils/test-connector-remote/build.gradle.kts @@ -0,0 +1,42 @@ +val edcVersion: String by project +val edcGroup: String by project +val testcontainersVersion: String by project +val lombokVersion: String by project +val restAssured: String by project +val awaitilityVersion: String by project +val assertj: String by project + +plugins { + `java-library` +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly("org.projectlombok:lombok:${lombokVersion}") + + api("org.junit.jupiter:junit-jupiter-api:5.10.0") + implementation("org.apache.commons:commons-lang3:3.13.0") + + api("${edcGroup}:junit:${edcVersion}") + api("org.awaitility:awaitility:${awaitilityVersion}") + api(project(":utils:json-and-jsonld-utils")) + implementation("${edcGroup}:sql-core:${edcVersion}") + implementation("${edcGroup}:json-ld-spi:${edcVersion}") + implementation("${edcGroup}:json-ld:${edcVersion}") + implementation("org.assertj:assertj-core:${assertj}") + implementation("org.testcontainers:testcontainers:${testcontainersVersion}") + implementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") + implementation("org.testcontainers:postgresql:${testcontainersVersion}") + implementation("io.rest-assured:rest-assured:${restAssured}") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java new file mode 100644 index 000000000..e85911e77 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfig; +import de.sovity.edc.extension.e2e.connector.config.api.auth.NoneAuthProvider; +import io.restassured.http.Header; +import io.restassured.specification.RequestSpecification; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.contract.spi.ContractId; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.jsonld.util.JacksonJsonLd; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.eclipse.edc.spi.result.Failure; + +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.FINALIZED; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCAT_DATASET_ATTRIBUTE; +import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.ODRL_POLICY_ATTRIBUTE; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.eclipse.edc.spi.CoreConstants.EDC_PREFIX; + +@SuppressWarnings("java:S5960") +@RequiredArgsConstructor +public class ConnectorRemote { + @Getter + + private final ConnectorRemoteConfig config; + + private final ObjectMapper objectMapper = JacksonJsonLd.createObjectMapper(); + public final Duration timeout = Duration.ofSeconds(60); + private final JsonLd jsonLd = new TitaniumJsonLd(new ConsoleMonitor()); + + public void createAsset(String assetId, Map dataAddressProperties) { + var requestBody = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add("asset", createObjectBuilder() + .add(ID, assetId) + .add("properties", createObjectBuilder() + .add("description", "description"))) + .add("dataAddress", createObjectBuilder(dataAddressProperties)) + .build(); + + prepareManagementApiCall() + .contentType(JSON) + .body(requestBody) + .when() + .post("/v2/assets") + .then() + .statusCode(200) + .contentType(JSON); + } + + public List getAssetIds() { + var requestBody = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, EDC_NAMESPACE + "QuerySpec") + .build(); + return prepareManagementApiCall() + .contentType(JSON) + .body(requestBody) + .when() + .post("/v2/assets/request") + .then() + .statusCode(200) + .contentType(JSON) + .extract().jsonPath().getList("@id"); + } + + public String createPolicy(JsonObject policyJsonObject) { + var requestBody = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, EDC_NAMESPACE + "PolicyDefinition") + .add(EDC_NAMESPACE + "policy", policyJsonObject) + .build(); + + return prepareManagementApiCall() + .contentType(JSON) + .body(requestBody) + .when() + .post("/v2/policydefinitions") + .then() + .statusCode(200) + .contentType(JSON) + .extract().jsonPath().getString(ID); + } + + public void createContractDefinition( + String assetId, + String contractDefinitionId, + String accessPolicyId, + String contractPolicyId) { + var requestBody = createObjectBuilder() + .add(ID, contractDefinitionId) + .add(TYPE, EDC_NAMESPACE + "ContractDefinition") + .add(EDC_NAMESPACE + "accessPolicyId", accessPolicyId) + .add(EDC_NAMESPACE + "contractPolicyId", contractPolicyId) + .add(EDC_NAMESPACE + "assetsSelector", Json.createArrayBuilder() + .add(createObjectBuilder() + .add(TYPE, "CriterionDto") + .add(EDC_NAMESPACE + "operandLeft", EDC_NAMESPACE + "id") + .add(EDC_NAMESPACE + "operator", "=") + .add(EDC_NAMESPACE + "operandRight", assetId) + .build()) + .build()) + .build(); + + prepareManagementApiCall() + .contentType(JSON) + .body(requestBody) + .when() + .post("/v2/contractdefinitions") + .then() + .statusCode(200) + .contentType(JSON); + } + + public JsonArray getCatalogDatasets(URI providerProtocolEndpoint) { + var datasetReference = new AtomicReference(); + var requestBody = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, EDC_NAMESPACE + "CatalogRequest") + .add(EDC_NAMESPACE + "counterPartyAddress", providerProtocolEndpoint.toString()) + .add(EDC_NAMESPACE + "protocol", "dataspace-protocol-http") + .build(); + + await().atMost(timeout).untilAsserted(() -> { + var response = prepareManagementApiCall() + .contentType(JSON) + .when() + .body(requestBody) + .post("/v2/catalog/request") + .then() + .statusCode(200) + .extract().body().asString(); + + var responseBody = objectMapper.readValue(response, JsonObject.class); + + var catalog = jsonLd.expand(responseBody).orElseThrow(this::throwFailure); + + var datasets = catalog.getJsonArray(DCAT_DATASET_ATTRIBUTE); + assertThat(datasets).isNotEmpty(); + + datasetReference.set(datasets); + }); + + return datasetReference.get(); + } + + public JsonObject getDatasetForAsset(String assetId, URI providerProtocolEndpoint) { + var datasets = getCatalogDatasets(providerProtocolEndpoint); + return datasets.stream() + .map(JsonValue::asJsonObject) + .filter(it -> assetId.equals(getDatasetContractId(it).assetIdPart())) + .findFirst() + .orElseThrow(() -> new EdcException("No dataset for asset %s in the catalog".formatted(assetId))); + } + + public String negotiateContract( + String providerParticipantId, + URI providerProtocolEndpoint, + String offerId, + String assetId, + JsonObject policy) { + var requestBody = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, EDC_NAMESPACE + "ContractRequest") + .add(EDC_NAMESPACE + "consumerId", config.getParticipantId()) + .add(EDC_NAMESPACE + "providerId", providerParticipantId) + .add(EDC_NAMESPACE + "connectorAddress", providerProtocolEndpoint.toString()) + .add(EDC_NAMESPACE + "protocol", "dataspace-protocol-http") + .add(EDC_NAMESPACE + "offer", createObjectBuilder() + .add(EDC_NAMESPACE + "offerId", offerId) + .add(EDC_NAMESPACE + "assetId", assetId) + .add(EDC_NAMESPACE + "policy", jsonLd.compact(policy).orElseThrow(this::throwFailure)) + ) + .build(); + + var negotiationId = prepareManagementApiCall() + .contentType(JSON) + .body(requestBody) + .when() + .post("/v2/contractnegotiations") + .then() + .statusCode(200) + .extract().body().jsonPath().getString(ID); + + await().atMost(timeout).untilAsserted(() -> { + var state = getContractNegotiationState(negotiationId); + assertThat(state).isEqualTo(FINALIZED.name()); + }); + + return getContractAgreementId(negotiationId); + } + + public String getContractAgreementId(String negotiationId) { + var contractAgreementId = new AtomicReference(); + + await().atMost(timeout).untilAsserted(() -> { + var agreementId = getContractNegotiationField(negotiationId); + assertThat(agreementId).isNotNull().isInstanceOf(String.class); + + contractAgreementId.set(agreementId); + }); + + var id = contractAgreementId.get(); + assertThat(id).isNotEmpty(); + return id; + } + + private String getContractNegotiationField(String negotiationId) { + return prepareManagementApiCall() + .contentType(JSON) + .when() + .get("/v2/contractnegotiations/{id}", negotiationId) + .then() + .statusCode(200) + .extract().body().jsonPath() + .getString("'edc:contractAgreementId'"); + } + + public String getContractNegotiationState(String id) { + return prepareManagementApiCall() + .contentType(JSON) + .when() + .get("/v2/contractnegotiations/{id}/state", id) + .then() + .statusCode(200) + .extract().body().jsonPath().getString("'edc:state'"); + } + + public String getParticipantId() { + return config.getParticipantId(); + } + + public String initiateTransfer( + String contractAgreementId, + String assetId, + URI providerProtocolApi, + JsonObject destination) { + var requestBody = createObjectBuilder() + .add(TYPE, EDC_NAMESPACE + "TransferRequest") + .add(EDC_NAMESPACE + "protocol", "dataspace-protocol-http") + .add(EDC_NAMESPACE + "connectorAddress", providerProtocolApi.toString()) + .add(EDC_NAMESPACE + "connectorId", config.getParticipantId()) + .add(EDC_NAMESPACE + "assetId", assetId) + .add(EDC_NAMESPACE + "dataDestination", destination) + .add(EDC_NAMESPACE + "contractId", contractAgreementId) + .add(EDC_NAMESPACE + "privateProperties", Json.createObjectBuilder().build()) + .add(EDC_NAMESPACE + "managedResources", false) + .build(); + + return prepareManagementApiCall() + .contentType(JSON) + .body(requestBody) + .when() + .post("/v2/transferprocesses") + .then() + .statusCode(200) + .extract().body().jsonPath().getString(ID); + } + + public String consumeOffer( + String providerId, + URI providerProtocolApi, + String assetId, + JsonObject destination) { + var dataset = getDatasetForAsset(assetId, providerProtocolApi); + var contractId = getDatasetContractId(dataset); + var policy = dataset.getJsonArray(ODRL_POLICY_ATTRIBUTE).get(0).asJsonObject(); + + var contractAgreementId = negotiateContract( + providerId, + providerProtocolApi, + contractId.toString(), + contractId.assetIdPart(), + policy); + + var transferProcessId = initiateTransfer( + contractAgreementId, + assetId, + providerProtocolApi, + destination); + + assertThat(transferProcessId).isNotNull(); + return transferProcessId; + } + + public String getTransferProcessState(String id) { + return prepareManagementApiCall() + .contentType(JSON) + .when() + .get("/v2/transferprocesses/{id}/state", id) + .then() + .statusCode(200) + .extract().body().jsonPath().getString("'edc:state'"); + } + + public void createDataOffer( + String assetId, + String targetUrl + ) { + Map dataSource = Map.of( + EDC_NAMESPACE + "type", "HttpData", + EDC_NAMESPACE + "baseUrl", targetUrl, + EDC_NAMESPACE + "proxyQueryParams", "true" + ); + + var policy = createObjectBuilder() + .add(TYPE, "use") + .build(); + + var contractDefinitionId = UUID.randomUUID().toString(); + createAsset(assetId, dataSource); + var noConstraintPolicyId = createPolicy(policy); + createContractDefinition( + assetId, + contractDefinitionId, + noConstraintPolicyId, + noConstraintPolicyId); + } + + public RequestSpecification prepareManagementApiCall() { + var managementConfig = config.getManagementEndpoint(); + var managementBaseUri = managementConfig.getUri().toString(); + if (managementConfig.authProvider() instanceof NoneAuthProvider) { + return given().baseUri(managementBaseUri); + } + return given() + .baseUri(managementBaseUri) + .header(getAuthHeader()); + } + + private Header getAuthHeader() { + var authProvider = config.getManagementEndpoint().authProvider(); + if ("".equals(authProvider.getAuthorizationHeader())) { + return null; + } + return new Header( + authProvider.getAuthorizationHeader(), + authProvider.getAuthorizationHeaderValue()); + } + + + public ContractId getDatasetContractId(JsonObject dataset) { + var id = dataset.getJsonArray(ODRL_POLICY_ATTRIBUTE).get(0).asJsonObject().getString(ID); + return ContractId.parseId(id).orElseThrow(this::throwFailure); + } + + private RuntimeException throwFailure(Failure failure) { + return new IllegalStateException(failure.getFailureDetail()); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java new file mode 100644 index 000000000..88089a752 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector; + +import jakarta.json.JsonObject; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.Duration; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; + +@SuppressWarnings("java:S5960") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DataTransferTestUtil { + + public static final Duration TIMEOUT = Duration.ofSeconds(30); + + public static JsonObject buildDataAddressJsonLd(String baseUrl, String method) { + return createObjectBuilder() + .add(TYPE, EDC_NAMESPACE + "DataAddress") + .add(EDC_NAMESPACE + "type", "HttpData") + .add(EDC_NAMESPACE + "properties", createObjectBuilder() + .add(EDC_NAMESPACE + "baseUrl", baseUrl) + .add(EDC_NAMESPACE + "method", method) + .build()) + .build(); + } + + public static Map buildDataAddressProperties(String baseUrl, String method) { + return Map.of( + EDC_NAMESPACE + "type", "HttpData", + EDC_NAMESPACE + "baseUrl", baseUrl, + EDC_NAMESPACE + "method", method + ); + } + + + public static void validateDataTransferred(String checkUrl, String expectedData) { + await().atMost(TIMEOUT).untilAsserted(() -> { + var actual = + when() + .get(checkUrl) + .then() + .statusCode(200) + .extract().body().asString(); + assertThat(actual).isEqualTo(expectedData); + }); + } + + public static void validateDataTransferred(String checkUrl, Map params, String expected) { + await().atMost(TIMEOUT).untilAsserted(() -> { + var actual = + given().params(params).when() + .get(checkUrl) + .then() + .statusCode(200) + .extract().body().asString(); + assertThat(actual).isEqualTo(expected); + }); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java new file mode 100644 index 000000000..e67af343b --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector; + +import de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroupConfig; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.buildDataAddressJsonLd; +import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.buildDataAddressProperties; + +@RequiredArgsConstructor +public class MockDataAddressRemote { + private final EdcApiGroupConfig defaultEndpoint; + + public String getDataSinkUrl() { + return getMockBackendUrl("data-sink"); + } + + public String getDataSinkSpyUrl() { + return getMockBackendUrl("data-sink/spy"); + } + + public String getDataSourceUrl(String data) { + return getMockBackendUrl("data-source?data=%s".formatted(data)); + } + + public String getDataSourceQueryParamsUrl() { + return getMockBackendUrl("data-source/echo-query-params"); + } + + public String getMockBackendUrl(String path) { + return "%s/test-backend/%s".formatted(defaultEndpoint.getUri().toString(), path); + } + + public JsonObject getDataSinkJsonLd() { + return buildDataAddressJsonLd(getDataSinkUrl(), "PUT"); + } + + public Map getDataSinkProperties() { + return buildDataAddressProperties(getDataSinkUrl(), "PUT"); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java new file mode 100644 index 000000000..ff34e619b --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config; + +import de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroupConfig; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Map; + + +@Data +@AllArgsConstructor +public class ConnectorConfig { + private String participantId; + private EdcApiGroupConfig defaultEndpoint; + private EdcApiGroupConfig managementEndpoint; + private EdcApiGroupConfig protocolEndpoint; + private Map properties; + + public void setProperty(String key, String value) { + properties.put(key, value); + } + + public void setProperties(Map properties) { + this.properties.putAll(properties); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java new file mode 100644 index 000000000..5cc358f9a --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config; + +import de.sovity.edc.extension.e2e.db.TestDatabase; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.UUID; + +import static de.sovity.edc.extension.e2e.connector.config.DatasourceConfigUtils.configureDatasources; +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiConfigFactory.configureApi; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ConnectorConfigFactory { + + public static ConnectorConfig forTestDatabase(String participantId, int firstPort, TestDatabase testDatabase) { + var config = basicEdcConfig(participantId, firstPort); + config.setProperties(configureDatasources(testDatabase.getJdbcCredentials())); + return config; + } + + public static ConnectorConfig basicEdcConfig(String participantId, int firstPort) { + var apiKey = UUID.randomUUID().toString(); + var apiConfig = configureApi(firstPort, apiKey); + + var properties = new HashMap(); + properties.put("edc.participant.id", participantId); + properties.put("edc.api.auth.key", apiKey); + properties.put("edc.dsp.callback.address", apiConfig.getProtocolApiGroup().getUri().toString()); + properties.putAll(apiConfig.getProperties()); + + properties.put("edc.jsonld.https.enabled", "true"); + properties.put("edc.last.commit.info", "test env commit message"); + properties.put("edc.build.date", "2023-05-08T15:30:00Z"); + + properties.put("my.edc.participant.id", participantId); + properties.put("my.edc.title", "Connector Title %s".formatted(participantId)); + properties.put("my.edc.description", "Connector Description %s".formatted(participantId)); + properties.put("my.edc.curator.url", "http://curator.%s".formatted(participantId)); + properties.put("my.edc.curator.name", "Curator Name %s".formatted(participantId)); + properties.put("my.edc.maintainer.url", "http://maintainer.%s".formatted(participantId)); + properties.put("my.edc.maintainer.name", "Maintainer Name %s".formatted(participantId)); + + return new ConnectorConfig( + participantId, + apiConfig.getDefaultApiGroup(), + apiConfig.getManagementApiGroup(), + apiConfig.getProtocolApiGroup(), + properties + ); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java new file mode 100644 index 000000000..69dbcf302 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config; + +import de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroupConfig; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + + +@Getter +@RequiredArgsConstructor +public class ConnectorRemoteConfig { + private final String participantId; + private final EdcApiGroupConfig defaultEndpoint; + private final EdcApiGroupConfig managementEndpoint; + private final EdcApiGroupConfig protocolEndpoint; +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java new file mode 100644 index 000000000..54f08c8fc --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config; + +import de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroup; +import de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroupConfig; +import de.sovity.edc.extension.e2e.connector.config.api.auth.ApiKeyAuthProvider; +import de.sovity.edc.extension.e2e.connector.config.api.auth.NoneAuthProvider; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiConfigFactory.fromUri; +import static de.sovity.edc.extension.e2e.env.EnvUtil.getEnvVar; +import static de.sovity.edc.extension.e2e.env.EnvUtil.getEnvVarUri; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ConnectorRemoteConfigFactory { + public static final String EDC_DEFAULT_URL = "%s_EDC_DEFAULT_URL"; + public static final String EDC_MANAGEMENT_URL = "%s_EDC_MANAGEMENT_URL"; + public static final String EDC_PROTOCOL_URL = "%s_EDC_PROTOCOL_URL"; + public static final String EDC_MANAGEMENT_AUTH_HEADER = "%s_EDC_MANAGEMENT_AUTH_HEADER"; + public static final String EDC_MANAGEMENT_AUTH_VALUE = "%s_EDC_MANAGEMENT_AUTH_VALUE"; + public static final String TEST_BACKEND_DEFAULT_ENDPOINT = "TEST_BACKEND_DEFAULT_ENDPOINT"; + + /** + * Access a locally launched connector + * + * @param connectorConfig connector config + * @return connector remote config + */ + public static ConnectorRemoteConfig fromConnectorConfig(ConnectorConfig connectorConfig) { + return new ConnectorRemoteConfig( + connectorConfig.getParticipantId(), + connectorConfig.getDefaultEndpoint(), + connectorConfig.getManagementEndpoint(), + connectorConfig.getProtocolEndpoint() + ); + } + + /** + * Access a connector started externally, e.g. by a Github Pipeline + * + * @param participantId participant id (prefix for env vars) + * @return connector remote config + */ + public static ConnectorRemoteConfig getFromEnv(String participantId) { + return new ConnectorRemoteConfig( + participantId, + getDefaultApiGroupConfig(participantId), + getManagementApiGroupConfig(participantId), + getProtocolApiGroupConfig(participantId) + ); + } + + private static EdcApiGroupConfig getDefaultApiGroupConfig(String participantId) { + var uri = getEnvVarUri(EDC_DEFAULT_URL.formatted(participantId)); + return fromUri(EdcApiGroup.DEFAULT, uri, new NoneAuthProvider()); + } + + private static EdcApiGroupConfig getProtocolApiGroupConfig(String participantId) { + var uri = getEnvVarUri(EDC_PROTOCOL_URL.formatted(participantId)); + return fromUri(EdcApiGroup.PROTOCOL, uri, new NoneAuthProvider()); + } + + private static EdcApiGroupConfig getManagementApiGroupConfig(String participantId) { + var uri = getEnvVarUri(EDC_MANAGEMENT_URL.formatted(participantId)); + var auth = new ApiKeyAuthProvider( + getEnvVar(EDC_MANAGEMENT_AUTH_HEADER.formatted(participantId)), + getEnvVar(EDC_MANAGEMENT_AUTH_VALUE.formatted(participantId))); + return fromUri(EdcApiGroup.MANAGEMENT, uri, auth); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java new file mode 100644 index 000000000..fd1df4057 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config; + +import de.sovity.edc.extension.e2e.db.JdbcCredentials; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DatasourceConfigUtils { + private static final List DATASOURCE_NAMES = List.of( + "default", + "asset", + "contractdefinition", + "contractnegotiation", + "policy", + "transferprocess", + "dataplaneinstance" + ); + + public static Map configureDatasources(JdbcCredentials credentials) { + var properties = new HashMap(); + properties.put("edc.flyway.clean.enable", "true"); + properties.put("edc.flyway.clean", "true"); + DATASOURCE_NAMES.forEach(name -> { + properties.put("edc.datasource.%s.name".formatted(name), name); + properties.put("edc.datasource.%s.url".formatted(name), credentials.jdbcUrl()); + properties.put("edc.datasource.%s.user".formatted(name), credentials.jdbcUser()); + properties.put("edc.datasource.%s.password".formatted(name), credentials.jdbcPassword()); + }); + return properties; + } + +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java new file mode 100644 index 000000000..d1cc4dd36 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api; + + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Value +@Builder +public class EdcApiConfig { + EdcApiGroupConfig defaultApiGroup; + EdcApiGroupConfig protocolApiGroup; + EdcApiGroupConfig managementApiGroup; + EdcApiGroupConfig controlApiGroup; + + public Map getProperties() { + return Stream.of(defaultApiGroup, protocolApiGroup, managementApiGroup, controlApiGroup) + .map(EdcApiGroupConfig::getProperties) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java new file mode 100644 index 000000000..603172ea4 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api; + +import de.sovity.edc.extension.e2e.connector.config.api.auth.ApiKeyAuthProvider; +import de.sovity.edc.extension.e2e.connector.config.api.auth.AuthProvider; +import de.sovity.edc.extension.e2e.connector.config.api.auth.NoneAuthProvider; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.net.URI; + +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroup.CONTROL; +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroup.DEFAULT; +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroup.MANAGEMENT; +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiGroup.PROTOCOL; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EdcApiConfigFactory { + private static final String BASE_URL = "http://localhost"; + + /** + * Configures EDC API Endpoints by conventions with given port offset. + * + * @param firstPort port offset + * @param managementApiKey management API key + * @return {@link EdcApiConfig} + */ + public static EdcApiConfig configureApi(int firstPort, String managementApiKey) { + var defaultApiGroup = unprotected(DEFAULT, "/api", firstPort + 1); + var managementApiGroup = apiKeyAuth(MANAGEMENT, "/api/management", firstPort + 2, managementApiKey); + var protocolApiGroup = unprotected(PROTOCOL, "/api/dsp", firstPort + 3); + var unprotected = unprotected(CONTROL, "/api/control", firstPort + 4); + + return EdcApiConfig.builder() + .defaultApiGroup(defaultApiGroup) + .protocolApiGroup(protocolApiGroup) + .managementApiGroup(managementApiGroup) + .controlApiGroup(unprotected) + .build(); + } + + public static EdcApiGroupConfig fromUri(EdcApiGroup edcApiGroup, URI uri, AuthProvider authProvider) { + return new EdcApiGroupConfig( + edcApiGroup, + "%s://%s".formatted(uri.getScheme(), uri.getHost()), + uri.getPort(), + uri.getPath(), + authProvider + ); + } + + private static EdcApiGroupConfig unprotected( + EdcApiGroup edcApiGroup, + String path, + int port) { + return new EdcApiGroupConfig( + edcApiGroup, + BASE_URL, + port, + path, + new NoneAuthProvider() + ); + } + + private static EdcApiGroupConfig apiKeyAuth( + EdcApiGroup apiGroup, + String path, + int port, + String apiKey) { + return new EdcApiGroupConfig( + apiGroup, + BASE_URL, + port, + path, + new ApiKeyAuthProvider("X-Api-Key", apiKey)); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java new file mode 100644 index 000000000..3af481919 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public enum EdcApiGroup { + DEFAULT(""), + PROTOCOL("protocol"), + MANAGEMENT("management"), + CONTROL("control"); + + private final String name; + + public Map getProperties(String path, int port) { + if (this == EdcApiGroup.DEFAULT) { + return Map.of( + "web.http.path", path, + "web.http.port", String.valueOf(port) + ); + } else { + return Map.of( + "web.http.%s.path".formatted(name), path, + "web.http.%s.port".formatted(name), String.valueOf(port)); + } + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java new file mode 100644 index 000000000..78c610da5 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api; + +import de.sovity.edc.extension.e2e.connector.config.api.auth.AuthProvider; +import lombok.With; + +import java.net.URI; +import java.util.Map; + +public record EdcApiGroupConfig( + EdcApiGroup edcApiGroup, + String baseUrl, + int port, + String path, + @With + AuthProvider authProvider +) { + + public URI getUri() { + return URI.create("%s:%s%s".formatted(baseUrl, port, path)); + } + + public Map getProperties() { + return edcApiGroup.getProperties(path, port); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java new file mode 100644 index 000000000..f747ed6e2 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api.auth; + + +public record ApiKeyAuthProvider( + String headerName, + String apiKey) implements AuthProvider { + @Override + public String getAuthorizationHeader() { + return headerName; + } + + @Override + public String getAuthorizationHeaderValue() { + return apiKey; + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java new file mode 100644 index 000000000..35474d8a9 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api.auth; + +public interface AuthProvider { + + String getAuthorizationHeader(); + + String getAuthorizationHeaderValue(); +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java new file mode 100644 index 000000000..3181c9453 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.connector.config.api.auth; + +public class NoneAuthProvider implements AuthProvider { + @Override + public String getAuthorizationHeader() { + return ""; + } + + @Override + public String getAuthorizationHeaderValue() { + return ""; + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java new file mode 100644 index 000000000..81357af93 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.e2e.db; + +/** + * JDBC Credentials + * + * @param jdbcUrl JDBC URL without credentials + * @param jdbcUser JDBC User + * @param jdbcPassword JDBC Password + */ +public record JdbcCredentials( + String jdbcUrl, + String jdbcUser, + String jdbcPassword +) { +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java new file mode 100644 index 000000000..b5a009043 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.e2e.db; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { + + JdbcCredentials getJdbcCredentials(); + + @Override + default void afterAll(ExtensionContext extensionContext) throws Exception { + + } + + @Override + default void beforeAll(ExtensionContext extensionContext) throws Exception { + + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java new file mode 100644 index 000000000..862585fc8 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.e2e.db; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TestDatabaseFactory { + /** + * Returns a JUnit 5 Extension that either connects to a test database or launches a + * testcontainer. + * + * @return {@link TestDatabase} + */ + public static TestDatabase getTestDatabase(int iDatabase) { + if (TestDatabaseViaEnvVars.isSkipTestcontainers()) { + return new TestDatabaseViaEnvVars(iDatabase); + } + + return new TestDatabaseViaTestcontainers(); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java new file mode 100644 index 000000000..b66cf031f --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.e2e.db; + +import lombok.RequiredArgsConstructor; + +import static de.sovity.edc.extension.e2e.env.EnvUtil.getEnvVar; + +@RequiredArgsConstructor +public class TestDatabaseViaEnvVars implements TestDatabase { + public static final String SKIP_TESTCONTAINERS = "SKIP_TESTCONTAINERS"; + public static final String TEST_POSTGRES_JDBC_URL = "TEST_POSTGRES_%d_JDBC_URL"; + public static final String TEST_POSTGRES_JDBC_USER = "TEST_POSTGRES_%d_JDBC_USER"; + public static final String TEST_POSTGRES_JDBC_PASSWORD = "TEST_POSTGRES_%d_JDBC_PASSWORD"; + + private final int iDatabase; + + public JdbcCredentials getJdbcCredentials() { + return new JdbcCredentials( + getEnvVar(TEST_POSTGRES_JDBC_URL.formatted(iDatabase)), + getEnvVar(TEST_POSTGRES_JDBC_USER.formatted(iDatabase)), + getEnvVar(TEST_POSTGRES_JDBC_PASSWORD.formatted(iDatabase)) + ); + } + + public static boolean isSkipTestcontainers() { + return "true".equals(System.getenv(SKIP_TESTCONTAINERS)); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java new file mode 100644 index 000000000..5d25b1613 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.e2e.db; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.PostgreSQLContainer; + +public class TestDatabaseViaTestcontainers implements TestDatabase { + private static final String POSTGRES_USER = "postgres"; + private static final String POSTGRES_PASSWORD = "postgres"; + private static final String POSTGRES_DB = "edc"; + + private final PostgreSQLContainer container; + + public TestDatabaseViaTestcontainers() { + container = new PostgreSQLContainer<>("postgres:15-alpine") + .withUsername(POSTGRES_USER) + .withPassword(POSTGRES_PASSWORD) + .withDatabaseName(POSTGRES_DB); + } + + @Override + public void afterAll(ExtensionContext context) { + container.stop(); + } + + @Override + public void beforeAll(ExtensionContext context) { + container.start(); + } + @Override + public JdbcCredentials getJdbcCredentials() { + return new JdbcCredentials(container.getJdbcUrl(), container.getUsername(), container.getPassword()); + } + +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java new file mode 100644 index 000000000..1b1c1bd69 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.extension.e2e.env; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.net.URI; +import java.net.URISyntaxException; + +import static java.util.Objects.requireNonNull; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EnvUtil { + + public static String getEnvVar(String variableName) { + return requireNonNull(System.getenv(variableName), () -> + "Missing required environment variable: %s".formatted(variableName)); + } + + public static URI getEnvVarUri(String variableName) { + try { + return new URI(getEnvVar(variableName)); + } catch (URISyntaxException e) { + var message = "Failed reading environment variable as URI: %s".formatted(variableName); + throw new IllegalArgumentException(message, e); + } + } + +} diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts new file mode 100644 index 000000000..a58a24a96 --- /dev/null +++ b/utils/test-utils/build.gradle.kts @@ -0,0 +1,26 @@ +val edcVersion: String by project +val edcGroup: String by project +val testcontainersVersion: String by project +val lombokVersion: String by project +val restAssured: String by project +val awaitilityVersion: String by project +val assertj: String by project + +plugins { + `java-library` +} + +dependencies { + api("org.junit.jupiter:junit-jupiter-api:5.10.0") +} + +val sovityEdcExtensionGroup: String by project +group = sovityEdcExtensionGroup + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java new file mode 100644 index 000000000..6c91ac61c --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.extension.utils.junit; + +import org.junit.jupiter.api.Tag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a test must not execute on GitHub. + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Tag("not-on-github") +public @interface DisabledOnGithub { + +} From 6165ec1830c66769b720bb5e93f57d4f5820f676 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 2 May 2024 14:09:55 +0200 Subject: [PATCH 218/295] chore: Convert dependencies to use libs.version.toml (#908) * Add Azure repo for core-EDC fork * Prepare lib.toml --- .github/ISSUE_TEMPLATE/release.md | 4 +- build.gradle.kts | 5 +- .../{ => api}/eclipse-edc-management-api.yaml | 0 docs/{ => api}/postman_collection.json | 0 docs/{ => api}/sovity-edc-api-wrapper.yaml | 0 extensions/edc-ui-config/build.gradle.kts | 37 ++-- extensions/last-commit-info/build.gradle.kts | 43 ++-- .../policy-always-true/build.gradle.kts | 20 +- .../build.gradle.kts | 14 +- .../policy-time-interval/build.gradle.kts | 6 +- extensions/postgres-flyway/build.gradle.kts | 20 +- .../test-backend-controller/build.gradle.kts | 8 +- .../build.gradle.kts | 4 +- extensions/wrapper/README.md | 2 +- .../java-client-example/build.gradle.kts | 4 +- .../clients/java-client/build.gradle.kts | 57 +++--- .../wrapper/wrapper-api/build.gradle.kts | 36 ++-- .../wrapper-common-api/build.gradle.kts | 16 +- .../wrapper-common-mappers/build.gradle.kts | 38 ++-- .../wrapper/wrapper-ee-api/build.gradle.kts | 18 +- extensions/wrapper/wrapper/build.gradle.kts | 63 +++--- gradle/libs.versions.toml | 183 ++++++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- launchers/common/auth-daps/build.gradle.kts | 4 +- launchers/common/auth-mock/build.gradle.kts | 2 +- launchers/common/base-mds/build.gradle.kts | 2 +- launchers/common/base/build.gradle.kts | 32 +-- .../common/observability/build.gradle.kts | 2 +- launchers/connectors/mds-ce/build.gradle.kts | 2 +- .../connectors/sovity-ce/build.gradle.kts | 2 +- .../connectors/sovity-dev/build.gradle.kts | 2 +- .../connectors/test-backend/build.gradle.kts | 8 +- tests/build.gradle.kts | 22 +-- utils/catalog-parser/build.gradle.kts | 34 ++-- utils/json-and-jsonld-utils/build.gradle.kts | 36 ++-- utils/test-connector-remote/build.gradle.kts | 28 +-- 36 files changed, 463 insertions(+), 293 deletions(-) rename docs/{ => api}/eclipse-edc-management-api.yaml (100%) rename docs/{ => api}/postman_collection.json (100%) rename docs/{ => api}/sovity-edc-api-wrapper.yaml (100%) create mode 100644 gradle/libs.versions.toml diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index e6d651e53..6ff21b07a 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -49,7 +49,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Set the UI release version for `EDC_UI_IMAGE` of the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] If the Eclipse EDC version changed, update - the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-extensions/blob/main/docs/eclipse-edc-management-api.yaml). + the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-extensions/blob/main/docs/api/eclipse-edc-management-api.yaml). - [ ] Update the Postman Collection if required. - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-extensions/actions). @@ -60,7 +60,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Test with `EDC_UI_ACTIVE_PROFILE=sovity-open-source` - [ ] Test with `EDC_UI_ACTIVE_PROFILE=mds-open-source` - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. -- [ ] Test the postman collection against that running docker-compose. +- [ ] Test the [postman collection](../../docs/api/postman_collection.json) against that running docker-compose. - [ ] [Create a release](https://github.com/sovity/edc-extensions/releases/new) - [ ] In `Choose the tag`, type your new release version in the format `vx.y.z` (for instance `v1.2.3`) then click `+Create new tag vx.y.z on release`. - [ ] Re-use the changelog section as release description, and the version as title. diff --git a/build.gradle.kts b/build.gradle.kts index 678ef4a0d..06e204e1f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.gradle.internal.impldep.org.jsoup.safety.Safelist.basic plugins { id("java") @@ -9,8 +8,8 @@ plugins { } dependencies { - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } val downloadArtifact: Configuration by configurations.creating { diff --git a/docs/eclipse-edc-management-api.yaml b/docs/api/eclipse-edc-management-api.yaml similarity index 100% rename from docs/eclipse-edc-management-api.yaml rename to docs/api/eclipse-edc-management-api.yaml diff --git a/docs/postman_collection.json b/docs/api/postman_collection.json similarity index 100% rename from docs/postman_collection.json rename to docs/api/postman_collection.json diff --git a/docs/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml similarity index 100% rename from docs/sovity-edc-api-wrapper.yaml rename to docs/api/sovity-edc-api-wrapper.yaml diff --git a/extensions/edc-ui-config/build.gradle.kts b/extensions/edc-ui-config/build.gradle.kts index a5d78cdaf..56d0c920f 100644 --- a/extensions/edc-ui-config/build.gradle.kts +++ b/extensions/edc-ui-config/build.gradle.kts @@ -11,17 +11,17 @@ plugins { } dependencies { - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:control-plane-spi:${edcVersion}") - implementation("${edcGroup}:api-core:${edcVersion}") - implementation("${edcGroup}:management-api-configuration:${edcVersion}") + api(libs.edc.coreSpi) + api(libs.edc.controlPlaneSpi) + implementation(libs.edc.apiCore) + implementation(libs.edc.managementApiConfiguration) - implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation(libs.jakarta.rsApi) + implementation(libs.jakarta.validationApi) - testImplementation("${edcGroup}:control-plane-core:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") - testImplementation("${edcGroup}:http:${edcVersion}") { + testImplementation(libs.edc.controlPlaneCore) + testImplementation(libs.edc.junit) + testImplementation(libs.edc.http) { exclude(group = "org.eclipse.jetty", module = "jetty-client") exclude(group = "org.eclipse.jetty", module = "jetty-http") exclude(group = "org.eclipse.jetty", module = "jetty-io") @@ -31,18 +31,13 @@ dependencies { } // Updated jetty versions for e.g. CVE-2023-26048 - testImplementation("${jettyGroup}:jetty-client:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-http:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-io:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-server:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-util:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-webapp:${jettyVersion}") - - testImplementation("io.rest-assured:rest-assured:${restAssured}") - testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.bundles.jetty.cve2023) + + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.mockito.core) + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } val sovityEdcExtensionGroup: String by project diff --git a/extensions/last-commit-info/build.gradle.kts b/extensions/last-commit-info/build.gradle.kts index 80a8227cf..2d4f01a0a 100644 --- a/extensions/last-commit-info/build.gradle.kts +++ b/extensions/last-commit-info/build.gradle.kts @@ -12,23 +12,23 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:control-plane-spi:${edcVersion}") - implementation("${edcGroup}:api-core:${edcVersion}") - implementation("${edcGroup}:management-api-configuration:${edcVersion}") + api(libs.edc.coreSpi) + api(libs.edc.controlPlaneSpi) + implementation(libs.edc.apiCore) + implementation(libs.edc.managementApiConfiguration) - implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation(libs.jakarta.rsApi) + implementation(libs.jakarta.validationApi) - testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") - testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) - testImplementation("${edcGroup}:control-plane-core:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") - testImplementation("${edcGroup}:http:${edcVersion}") { + testImplementation(libs.edc.controlPlaneCore) + testImplementation(libs.edc.junit) + testImplementation(libs.edc.http) { exclude(group = "org.eclipse.jetty", module = "jetty-client") exclude(group = "org.eclipse.jetty", module = "jetty-http") exclude(group = "org.eclipse.jetty", module = "jetty-io") @@ -38,18 +38,13 @@ dependencies { } // Updated jetty versions for e.g. CVE-2023-26048 - testImplementation("${jettyGroup}:jetty-client:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-http:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-io:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-server:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-util:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-webapp:${jettyVersion}") + testImplementation(libs.bundles.jetty.cve2023) - testImplementation("io.rest-assured:rest-assured:${restAssured}") - testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.mockito.core) + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } val sovityEdcExtensionGroup: String by project diff --git a/extensions/policy-always-true/build.gradle.kts b/extensions/policy-always-true/build.gradle.kts index 0b1fc415f..9122cf0df 100644 --- a/extensions/policy-always-true/build.gradle.kts +++ b/extensions/policy-always-true/build.gradle.kts @@ -8,17 +8,17 @@ plugins { } dependencies { - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:policy-engine-spi:${edcVersion}") - api("${edcGroup}:control-plane-spi:${edcVersion}") - implementation("${edcGroup}:api-core:${edcVersion}") + api(libs.edc.coreSpi) + api(libs.edc.policyEngineSpi) + api(libs.edc.controlPlaneSpi) + implementation(libs.edc.apiCore) - testImplementation("${edcGroup}:control-plane-core:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") - testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.edc.controlPlaneCore) + testImplementation(libs.edc.junit) + testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.mockito.core) + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } val sovityEdcExtensionGroup: String by project diff --git a/extensions/policy-referring-connector/build.gradle.kts b/extensions/policy-referring-connector/build.gradle.kts index 972612083..04287b42f 100644 --- a/extensions/policy-referring-connector/build.gradle.kts +++ b/extensions/policy-referring-connector/build.gradle.kts @@ -9,14 +9,14 @@ plugins { } dependencies { - api("${edcGroup}:auth-spi:${edcVersion}") - api("${edcGroup}:policy-engine-spi:${edcVersion}") - api("${edcGroup}:contract-spi:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") + api(libs.edc.authSpi) + api(libs.edc.policyEngineSpi) + api(libs.edc.contractSpi) + testImplementation(libs.edc.junit) - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-params:${jupiterVersion}") + testImplementation(libs.mockito.core) + testImplementation(libs.junit.api) + testImplementation(libs.junit.params) } tasks.withType { diff --git a/extensions/policy-time-interval/build.gradle.kts b/extensions/policy-time-interval/build.gradle.kts index 4a79097d7..e36914450 100644 --- a/extensions/policy-time-interval/build.gradle.kts +++ b/extensions/policy-time-interval/build.gradle.kts @@ -7,9 +7,9 @@ plugins { } dependencies { - api("${edcGroup}:auth-spi:${edcVersion}") - api("${edcGroup}:policy-engine-spi:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") + api(libs.edc.authSpi) + api(libs.edc.policyEngineSpi) + testImplementation(libs.edc.junit) } val sovityEdcExtensionGroup: String by project diff --git a/extensions/postgres-flyway/build.gradle.kts b/extensions/postgres-flyway/build.gradle.kts index 203782838..42089ae9b 100644 --- a/extensions/postgres-flyway/build.gradle.kts +++ b/extensions/postgres-flyway/build.gradle.kts @@ -12,22 +12,22 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - implementation("${edcGroup}:core-spi:${edcVersion}") - implementation("${edcGroup}:sql-core:${edcVersion}") + implementation(libs.edc.coreSpi) + implementation(libs.edc.sqlCore) // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) - implementation("${edcGroup}:control-plane-sql:${edcVersion}") - implementation("${tractusGroup}:sql-pool:${tractusVersion}") - implementation("${edcGroup}:transaction-local:${edcVersion}") + implementation(libs.edc.controlPlaneSql) + implementation(libs.edc.transactionLocal) + implementation(libs.tractus.sqlPool) - implementation("org.postgresql:postgresql:${postgresVersion}") + implementation(libs.postgres) - implementation("org.flywaydb:flyway-core:${flywayVersion}") + implementation(libs.flyway.core) - testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation(libs.edc.junit) } val sovityEdcExtensionGroup: String by project diff --git a/extensions/test-backend-controller/build.gradle.kts b/extensions/test-backend-controller/build.gradle.kts index 56ffc60c7..41e9dee8a 100644 --- a/extensions/test-backend-controller/build.gradle.kts +++ b/extensions/test-backend-controller/build.gradle.kts @@ -6,9 +6,9 @@ plugins { } dependencies { - api("${edcGroup}:api-core:${edcVersion}") - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:http:${edcVersion}") + api(libs.edc.apiCore) + api(libs.edc.coreSpi) + api(libs.edc.http) } val sovityEdcExtensionGroup: String by project @@ -20,4 +20,4 @@ publishing { from(components["java"]) } } -} \ No newline at end of file +} diff --git a/extensions/transfer-process-status-checker/build.gradle.kts b/extensions/transfer-process-status-checker/build.gradle.kts index cf7424b81..5bc93ae54 100644 --- a/extensions/transfer-process-status-checker/build.gradle.kts +++ b/extensions/transfer-process-status-checker/build.gradle.kts @@ -7,8 +7,8 @@ plugins { } dependencies { - api("${edcGroup}:transfer-spi:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") + api(libs.edc.transferSpi) + testImplementation(libs.edc.junit) } val sovityEdcExtensionGroup: String by project diff --git a/extensions/wrapper/README.md b/extensions/wrapper/README.md index 85c01afdd..1fabd0c4b 100644 --- a/extensions/wrapper/README.md +++ b/extensions/wrapper/README.md @@ -21,7 +21,7 @@ We provide a full type-safe and opinionated API Wrapper for better access to the ## Explore Create and consume Data Offers using clean type-safe JSON REST APIs: -- [API Wrapper OpenAPI YAML](../../docs/sovity-edc-api-wrapper.yaml). +- [API Wrapper OpenAPI YAML](../../docs/api/sovity-edc-api-wrapper.yaml). - [Java API Client Library](./clients/java-client) - [TypeScript API Client Library](./clients/typescript-client) diff --git a/extensions/wrapper/clients/java-client-example/build.gradle.kts b/extensions/wrapper/clients/java-client-example/build.gradle.kts index f43ea26ca..8c670abe7 100644 --- a/extensions/wrapper/clients/java-client-example/build.gradle.kts +++ b/extensions/wrapper/clients/java-client-example/build.gradle.kts @@ -1,6 +1,6 @@ plugins { java - id("io.quarkus") version "2.16.6.Final" + alias(libs.plugins.quarkus) } repositories { @@ -13,7 +13,7 @@ val quarkusPlatformArtifactId = "quarkus-bom" val quarkusPlatformVersion = "2.16.6.Final" dependencies { - implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation(enforcedPlatform(libs.quarkus.bom)) implementation("io.quarkus:quarkus-arc") implementation("io.quarkus:quarkus-resteasy-reactive-jackson") diff --git a/extensions/wrapper/clients/java-client/build.gradle.kts b/extensions/wrapper/clients/java-client/build.gradle.kts index 844184df4..a6783b3d9 100644 --- a/extensions/wrapper/clients/java-client/build.gradle.kts +++ b/extensions/wrapper/clients/java-client/build.gradle.kts @@ -11,7 +11,7 @@ val jettyGroup: String by project plugins { `java-library` `maven-publish` - id("org.openapi.generator") version "6.6.0" + alias(libs.plugins.openapi.generator) } repositories { @@ -28,19 +28,19 @@ dependencies { } // Generated Client's Dependencies - implementation("io.swagger:swagger-annotations:1.6.11") - implementation("com.google.code.findbugs:jsr305:3.0.2") - implementation("com.squareup.okhttp3:okhttp:4.11.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") - implementation("com.google.code.gson:gson:2.10.1") - implementation("io.gsonfire:gson-fire:1.8.5") - implementation("org.openapitools:jackson-databind-nullable:0.2.6") - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("jakarta.annotation:jakarta.annotation-api:1.3.5") + implementation(libs.swagger.annotations) + implementation(libs.findbugs.jsr305) + implementation(libs.okhttp.okhttp) + implementation(libs.okhttp.loggingInterceptor) + implementation(libs.gson) + implementation(libs.gsonFire) + implementation(libs.openapi.jacksonDatabindNullable) + implementation(libs.apache.commonsLang) + implementation(libs.jakarta.annotation) // Lombok - compileOnly("org.projectlombok:lombok:${lombokVersion}") - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) } tasks.getByName("test") { @@ -49,7 +49,7 @@ tasks.getByName("test") { // Extract the openapi file from the JAR val openapiFile = "sovity-edc-api-wrapper.yaml" -task("extractOpenapiYaml") { +val extractOpenapiYaml by tasks.registering(Copy::class) { dependsOn(openapiYaml) into("${project.buildDir}") from(zipTree(openapiYaml.singleFile)) { @@ -58,24 +58,26 @@ task("extractOpenapiYaml") { } tasks.getByName("openApiGenerate") { - dependsOn("extractOpenapiYaml") + dependsOn(extractOpenapiYaml) generatorName.set("java") - configOptions.set(mutableMapOf( - "invokerPackage" to "de.sovity.edc.client.gen", - "apiPackage" to "de.sovity.edc.client.gen.api", - "modelPackage" to "de.sovity.edc.client.gen.model", - "caseInsensitiveResponseHeaders" to "true", - "additionalModelTypeAnnotations" to "@lombok.AllArgsConstructor\n@lombok.Builder", - "annotationLibrary" to "swagger1", - "hideGenerationTimestamp" to "true", - "useRuntimeException" to "true", - )) + configOptions.set( + mutableMapOf( + "invokerPackage" to "de.sovity.edc.client.gen", + "apiPackage" to "de.sovity.edc.client.gen.api", + "modelPackage" to "de.sovity.edc.client.gen.model", + "caseInsensitiveResponseHeaders" to "true", + "additionalModelTypeAnnotations" to "@lombok.AllArgsConstructor\n@lombok.Builder", + "annotationLibrary" to "swagger1", + "hideGenerationTimestamp" to "true", + "useRuntimeException" to "true", + ) + ) inputSpec.set("${project.buildDir}/${openapiFile}") outputDir.set("${project.buildDir}/generated/client-project") } -task("postprocessGeneratedClient") { +val postprocessGeneratedClient by tasks.registering(Copy::class) { dependsOn("openApiGenerate") from("${project.buildDir}/generated/client-project/src/main/java") @@ -109,6 +111,10 @@ java { withJavadocJar() } +tasks.getByName("sourcesJar") { + dependsOn(postprocessGeneratedClient) +} + tasks.withType { val fullOptions = this.options as StandardJavadocDocletOptions fullOptions.tags = listOf("http.response.details:a:Http Response Details") @@ -118,7 +124,6 @@ tasks.withType { val sovityEdcGroup: String by project group = sovityEdcGroup - publishing { publications { create(project.name) { diff --git a/extensions/wrapper/wrapper-api/build.gradle.kts b/extensions/wrapper/wrapper-api/build.gradle.kts index 2c9b125e6..5ddf07630 100644 --- a/extensions/wrapper/wrapper-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-api/build.gradle.kts @@ -10,34 +10,34 @@ val jettyGroup: String by project plugins { `java-library` `maven-publish` - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.14" //./gradlew clean resolve - id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI - id("org.openapi.generator") version "6.6.0" //./gradlew openApiValidate && ./gradlew openApiGenerate + alias(libs.plugins.swagger.plugin) //./gradlew clean resolve + alias(libs.plugins.hidetake.swaggerGenerator) //./gradlew generateSwaggerUI + alias(libs.plugins.openapi.generator) //./gradlew openApiValidate && ./gradlew openApiGenerate } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) api(project(":extensions:wrapper:wrapper-common-api")) api(project(":extensions:wrapper:wrapper-common-mappers")) api(project(":extensions:wrapper:wrapper-ee-api")) - implementation("jakarta.validation:jakarta.validation-api:3.0.2") - implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") - implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") - implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") - implementation("jakarta.validation:jakarta.validation-api:3.0.2") - implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("org.apache.commons:commons-lang3:3.13.0") + implementation(libs.jakarta.validationApi) + implementation(libs.jakarta.rsApi) + implementation(libs.swagger.annotationsJakarta) + implementation(libs.swagger.jaxrs2Jakarta) + implementation(libs.jakarta.servlet) + implementation(libs.jakarta.validationApi) + implementation(libs.jakarta.rsApi) + implementation(libs.apache.commonsLang) } val openapiFileDir = "${project.buildDir}/swagger" val openapiFileFilename = "sovity-edc-api-wrapper.yaml" val openapiFile = "$openapiFileDir/$openapiFileFilename" -val openapiDocsDir = project.rootProject.rootDir.resolve("docs") +val openapiDocsDir = project.rootProject.rootDir.resolve("docs").resolve("api") tasks.withType { outputDir = file(openapiFileDir) @@ -49,15 +49,15 @@ tasks.withType { resourcePackages = setOf("de.sovity.edc.ext.wrapper.api") } -tasks.register("copyOpenapiYamlToDocs") { +val copyOpenapiYamlToDocs by tasks.registering(Copy::class) { dependsOn("resolve") from(openapiFile) into(openapiDocsDir) } -task("openApiGenerateTypeScriptClient") { +val openApiGenerateTypeScriptClient by tasks.registering(org.openapitools.generator.gradle.plugin.tasks.GenerateTask::class) { dependsOn("resolve") - dependsOn("copyOpenapiYamlToDocs") + dependsOn(copyOpenapiYamlToDocs) generatorName.set("typescript-fetch") configOptions.set(mutableMapOf( "supportsES6" to "true", @@ -80,7 +80,7 @@ task("openApiGenera tasks.withType { dependsOn("resolve") - dependsOn("openApiGenerateTypeScriptClient") + dependsOn(openApiGenerateTypeScriptClient) from(openapiFileDir) { include(openapiFileFilename) } diff --git a/extensions/wrapper/wrapper-common-api/build.gradle.kts b/extensions/wrapper/wrapper-common-api/build.gradle.kts index b4856c153..083561a82 100644 --- a/extensions/wrapper/wrapper-common-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-api/build.gradle.kts @@ -6,16 +6,16 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") - api("jakarta.servlet:jakarta.servlet-api:5.0.0") + api(libs.jakarta.rsApi) + api(libs.jakarta.validationApi) + api(libs.swagger.annotationsJakarta) + api(libs.swagger.jaxrs2Jakarta) + api(libs.jakarta.servlet) - implementation("org.apache.commons:commons-lang3:3.13.0") + implementation(libs.apache.commonsLang) } val sovityEdcGroup: String by project diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts index 47a6496c8..71fd5b3f8 100644 --- a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -12,30 +12,30 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - api("${edcGroup}:policy-model:${edcVersion}") - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:transform-core:${edcVersion}") - api("${edcGroup}:transform-spi:${edcVersion}") + api(libs.edc.policyModel) + api(libs.edc.coreSpi) + api(libs.edc.transformCore) + api(libs.edc.transformSpi) api(project(":extensions:wrapper:wrapper-common-api")) api(project(":utils:json-and-jsonld-utils")) - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("org.apache.commons:commons-collections4:4.4") - implementation("com.vladsch.flexmark:flexmark-all:0.64.8") + implementation(libs.apache.commonsLang) + implementation(libs.apache.commonsCollections) + implementation(libs.flexmark.all) - testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") - testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) testImplementation(project(":utils:test-utils")) - testImplementation("${edcGroup}:json-ld:${edcVersion}") - testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.mockito:mockito-inline:${mockitoVersion}") - testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.edc.jsonLd) + testImplementation(libs.jsonUnit.assertj) + testImplementation(libs.assertj.core) + testImplementation(libs.junit.api) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockito.junitJupiter) + testRuntimeOnly(libs.junit.engine) } val sovityEdcGroup: String by project diff --git a/extensions/wrapper/wrapper-ee-api/build.gradle.kts b/extensions/wrapper/wrapper-ee-api/build.gradle.kts index eab881398..bc2cae006 100644 --- a/extensions/wrapper/wrapper-ee-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-ee-api/build.gradle.kts @@ -6,19 +6,19 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) api(project(":extensions:wrapper:wrapper-common-api")) - api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.15") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.15") - api("jakarta.servlet:jakarta.servlet-api:5.0.0") + api(libs.jakarta.rsApi) + api(libs.jakarta.validationApi) + api(libs.swagger.annotationsJakarta) + api(libs.swagger.jaxrs2Jakarta) + api(libs.jakarta.servlet) - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("org.glassfish.jersey.media:jersey-media-multipart:3.1.3") + implementation(libs.apache.commonsLang) + implementation(libs.jersey.mediaMultipart) } val sovityEdcGroup: String by project diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts index 40681b945..c0ebb5be7 100644 --- a/extensions/wrapper/wrapper/build.gradle.kts +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -14,34 +14,34 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - implementation("${edcGroup}:api-core:${edcVersion}") - implementation("${edcGroup}:management-api-configuration:${edcVersion}") - implementation("${edcGroup}:dsp-http-spi:${edcVersion}") + implementation(libs.edc.apiCore) + implementation(libs.edc.managementApiConfiguration) + implementation(libs.edc.dspHttpSpi) api(project(":extensions:wrapper:wrapper-api")) api(project(":extensions:wrapper:wrapper-common-mappers")) api(project(":utils:catalog-parser")) api(project(":utils:json-and-jsonld-utils")) - api("${edcGroup}:contract-definition-api:${edcVersion}") - api("${edcGroup}:control-plane-spi:${edcVersion}") - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:policy-definition-api:${edcVersion}") - api("${edcGroup}:transfer-process-api:${edcVersion}") - implementation("org.apache.commons:commons-lang3:3.13.0") + api(libs.edc.contractDefinitionApi) + api(libs.edc.controlPlaneSpi) + api(libs.edc.coreSpi) + api(libs.edc.policyDefinitionApi) + api(libs.edc.transferProcessApi) + implementation(libs.apache.commonsLang) - testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") - testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) testImplementation(project(":extensions:wrapper:clients:java-client")) testImplementation(project(":extensions:policy-always-true")) testImplementation(project(":utils:test-utils")) - testImplementation("${edcGroup}:control-plane-core:${edcVersion}") - testImplementation("${edcGroup}:dsp:${edcVersion}") - testImplementation("${edcGroup}:iam-mock:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") - testImplementation("${edcGroup}:http:${edcVersion}") { + testImplementation(libs.edc.controlPlaneCore) + testImplementation(libs.edc.dsp) + testImplementation(libs.edc.iamMock) + testImplementation(libs.edc.junit) + testImplementation(libs.edc.http) { exclude(group = "org.eclipse.jetty", module = "jetty-client") exclude(group = "org.eclipse.jetty", module = "jetty-http") exclude(group = "org.eclipse.jetty", module = "jetty-io") @@ -51,24 +51,19 @@ dependencies { } // Updated jetty versions for e.g. CVE-2023-26048 - testImplementation("${jettyGroup}:jetty-client:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-http:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-io:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-server:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-util:${jettyVersion}") - testImplementation("${jettyGroup}:jetty-webapp:${jettyVersion}") + testImplementation(libs.bundles.jetty.cve2023) - testImplementation("${edcGroup}:json-ld:${edcVersion}") - testImplementation("${edcGroup}:dsp-http-spi:${edcVersion}") - testImplementation("${edcGroup}:dsp-api-configuration:${edcVersion}") - testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") + testImplementation(libs.edc.jsonLd) + testImplementation(libs.edc.dspHttpSpi) + testImplementation(libs.edc.dspApiConfiguration) + testImplementation(libs.edc.dataPlaneSelectorCore) - testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") - testImplementation("io.rest-assured:rest-assured:${restAssured}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.jsonUnit.assertj) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.mockito.core) + testImplementation(libs.assertj.core) + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } tasks.withType { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..ca6376cf0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,183 @@ +[versions] +assertj = "3.23.1" +awaitility = "4.2.0" +commonsCompress = "1.26.1" +commonsCollections = "4.4" +commonsIo = "2.13.0" +commonsLang = "3.13.0" +edc = "0.2.1.2" +findbugs = "3.0.2" +flexmark = "0.64.8" +flyway = "9.0.1" +gson = "2.10.1" +gsonFire = "1.8.5" +guava = "33.1.0-jre" +hidetakeSwagger = "2.19.2" +jakartaAnnotation = "1.3.5" +jakartaJson = "2.0.1" +jakartaRs = "3.1.0" +jakartaServlet = "5.0.0" +jakartaValidation = "3.0.2" +java = "17" +jersey = "3.1.3" +jetty = "11.0.15" +jsonUnit = "3.2.7" +junit = "5.10.0" +loggingHouse = "0.2.10" +lombok = "1.18.28" +mockito = "4.8.0" +mockserver = "5.15.0" +okhttp = "4.11.0" +okio = "3.9.0" +openapiGenerator = "6.6.0" +openapiJackson = "0.2.6" +postgres = "42.4.0" +quarkus = "2.16.6.Final" +restAssured = "4.5.0" +retry = "1.5.7" +shadow = "7.1.2" +swagger = "1.6.11" +swaggerCore = "2.2.15" +testcontainers = "1.17.6" +titaniumLd = "1.3.2" +tractus = "0.5.3" + +[libraries] + +apache-commonsCollections = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections" } +apache-commonsCompress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" } +apache-commonsIo = { module = "commons-io:commons-io", version.ref = "commonsIo" } +apache-commonsLang = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang" } + +apicatalog-titaniumJsonLd = { module = "com.apicatalog:titanium-json-ld", version.ref = "titaniumLd" } + +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } + +awaitility-java = { module = "org.awaitility:awaitility", version.ref = "awaitility" } + +edc-apiCore = { module = "org.eclipse.edc:api-core", version.ref = "edc" } +edc-apiObservability = { module = "org.eclipse.edc:api-observability", version.ref = "edc" } +edc-authSpi = { module = "org.eclipse.edc:auth-spi", version.ref = "edc" } +edc-authTokenbased = { module = "org.eclipse.edc:auth-tokenbased", version.ref = "edc" } +edc-boot = { module = "org.eclipse.edc:boot", version.ref = "edc" } +edc-configurationFilesystem = { module = "org.eclipse.edc:configuration-filesystem", version.ref = "edc" } +edc-contractDefinitionApi = { module = "org.eclipse.edc:contract-definition-api", version.ref = "edc" } +edc-contractSpi = { module = "org.eclipse.edc:contract-spi", version.ref = "edc" } +edc-controlPlaneAggregateServices = { module = "org.eclipse.edc:control-plane-aggregate-services", version.ref = "edc" } +edc-controlPlaneCore = { module = "org.eclipse.edc:control-plane-core", version.ref = "edc" } +edc-controlPlaneSpi = { module = "org.eclipse.edc:control-plane-spi", version.ref = "edc" } +edc-controlPlaneSql = { module = "org.eclipse.edc:control-plane-sql", version.ref = "edc" } +edc-coreSpi = { module = "org.eclipse.edc:core-spi", version.ref = "edc" } +edc-dataPlaneCore = { module = "org.eclipse.edc:data-plane-core", version.ref = "edc" } +edc-dataPlaneFramework = { module = "org.eclipse.edc:data-plane-framework", version.ref = "edc" } +edc-dataPlaneHttp = { module = "org.eclipse.edc:data-plane-http", version.ref = "edc" } +edc-dataPlaneSelectorClient = { module = "org.eclipse.edc:data-plane-selector-client", version.ref = "edc" } +edc-dataPlaneSelectorCore = { module = "org.eclipse.edc:data-plane-selector-core", version.ref = "edc" } +edc-dataPlaneUtil = { module = "org.eclipse.edc:data-plane-util", version.ref = "edc" } +edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } +edc-dspApiConfiguration = { module = "org.eclipse.edc:dsp-api-configuration", version.ref = "edc" } +edc-dspHttpSpi = { module = "org.eclipse.edc:dsp-http-spi", version.ref = "edc" } +edc-http = { module = "org.eclipse.edc:http", version.ref = "edc" } +edc-iamMock = { module = "org.eclipse.edc:iam-mock", version.ref = "edc" } +edc-jsonLd = { module = "org.eclipse.edc:json-ld", version.ref = "edc" } +edc-jsonLdSpi = { module = "org.eclipse.edc:json-ld-spi", version.ref = "edc" } +edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } +edc-managementApi = { module = "org.eclipse.edc:management-api", version.ref = "edc" } +edc-managementApiConfiguration = { module = "org.eclipse.edc:management-api-configuration", version.ref = "edc" } +edc-monitorJdkLogger = { module = "org.eclipse.edc:monitor-jdk-logger", version.ref = "edc" } +edc-oauth2Core = { module = "org.eclipse.edc:oauth2-core", version.ref = "edc" } +edc-policyDefinitionApi = { module = "org.eclipse.edc:policy-definition-api", version.ref = "edc" } +edc-policyEngineSpi = { module = "org.eclipse.edc:policy-engine-spi", version.ref = "edc" } +edc-policyModel = { module = "org.eclipse.edc:policy-model", version.ref = "edc" } +edc-sqlCore = { module = "org.eclipse.edc:sql-core", version.ref = "edc" } +edc-transactionLocal = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } +edc-transferDataPlane = { module = "org.eclipse.edc:transfer-data-plane", version.ref = "edc" } +edc-transferProcessApi = { module = "org.eclipse.edc:transfer-process-api", version.ref = "edc" } +edc-transferSpi = { module = "org.eclipse.edc:transfer-spi", version.ref = "edc" } +edc-transformCore = { module = "org.eclipse.edc:transform-core", version.ref = "edc" } +edc-transformSpi = { module = "org.eclipse.edc:transform-spi", version.ref = "edc" } +edc-vaultFilesystem = { module = "org.eclipse.edc:vault-filesystem", version.ref = "edc" } + +findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs" } + +flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" } + +flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" } + +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } + +gsonFire = { module = "io.gsonfire:gson-fire", version.ref = "gsonFire" } + +guava = { module = "com.google.guava:guava", version.ref = "guava" } + +jakarta-annotation = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "jakartaAnnotation" } +jakarta-json = { module = "org.glassfish:jakarta.json", version.ref = "jakartaJson" } +jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "jakartaRs" } +jakarta-servlet = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakartaServlet" } +jakarta-validationApi = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakartaValidation" } + +jersey-mediaMultipart = { module = "org.glassfish.jersey.media:jersey-media-multipart", version.ref = "jersey" } + +jetty-client = { module = "org.eclipse.jetty:jetty-client", version.ref = "jetty" } +jetty-http = { module = "org.eclipse.jetty:jetty-http", version.ref = "jetty" } +jetty-io = { module = "org.eclipse.jetty:jetty-io", version.ref = "jetty" } +jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" } +jetty-util = { module = "org.eclipse.jetty:jetty-util", version.ref = "jetty" } +jetty-webapp = { module = "org.eclipse.jetty:jetty-webapp", version.ref = "jetty" } + +jsonUnit-assertj = { module = "net.javacrumbs.json-unit:json-unit-assertj", version.ref = "jsonUnit" } + +junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } +junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } + +loggingHouse-client = { module = "logging-house:logging-house-client", version.ref = "loggingHouse" } + +lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } + +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockito-junitJupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } + +mockserver-netty = { module = "org.mock-server:mockserver-netty", version.ref = "mockserver" } + +okhttp-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } + +okio-jvm = { module = "com.squareup.okio:okio-jvm", version.ref = "okio" } + +openapi-jacksonDatabindNullable = { module = "org.openapitools:jackson-databind-nullable", version.ref = "openapiJackson" } + +postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } + +quarkus-bom = { module = "io.quarkus.platform:quarkus-bom", version.ref = "quarkus" } + +restAssured-restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } + +swagger-annotations = { module = "io.swagger:swagger-annotations", version.ref = "swagger" } +swagger-annotationsJakarta = { module = "io.swagger.core.v3:swagger-annotations-jakarta", version.ref = "swaggerCore" } +swagger-jaxrs2Jakarta = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version.ref = "swaggerCore" } + +testcontainers-testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } +testcontainers-junitJupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } + +tractus-sqlPool = { module = "org.eclipse.tractusx.edc:sql-pool", version.ref = "tractus" } + +[bundles] +jetty-cve2023 = [ + "jetty-client", + "jetty-http", + "jetty-io", + "jetty-server", + "jetty-util", + "jetty-webapp", +] + +[plugins] +hidetake-swaggerGenerator = { id = "org.hidetake.swagger.generator", version.ref = "hidetakeSwagger" } +openapi-generator = { id = "org.openapi.generator", version.ref = "openapiGenerator" } +quarkus = { id = "io.quarkus", version.ref = "quarkus" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +swagger-plugin = { id = "io.swagger.core.v3.swagger-gradle-plugin", version.ref = "swaggerCore" } +retry = { id = "org.gradle.test-retry", version.ref = "retry" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb702f..48c0a02ca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/launchers/common/auth-daps/build.gradle.kts b/launchers/common/auth-daps/build.gradle.kts index f2323a3d3..18e774404 100644 --- a/launchers/common/auth-daps/build.gradle.kts +++ b/launchers/common/auth-daps/build.gradle.kts @@ -7,8 +7,8 @@ val edcGroup: String by project dependencies { // OAuth2 IAM - api("${edcGroup}:oauth2-core:${edcVersion}") - api("${edcGroup}:vault-filesystem:${edcVersion}") + api(libs.edc.oauth2Core) + api(libs.edc.vaultFilesystem) } val sovityEdcGroup: String by project diff --git a/launchers/common/auth-mock/build.gradle.kts b/launchers/common/auth-mock/build.gradle.kts index cf90a2430..bf7d74f56 100644 --- a/launchers/common/auth-mock/build.gradle.kts +++ b/launchers/common/auth-mock/build.gradle.kts @@ -7,7 +7,7 @@ val edcGroup: String by project dependencies { // Mock IAM - api("${edcGroup}:iam-mock:${edcVersion}") + api(libs.edc.iamMock) } val sovityEdcGroup: String by project diff --git a/launchers/common/base-mds/build.gradle.kts b/launchers/common/base-mds/build.gradle.kts index 6905a27c1..313ed948b 100644 --- a/launchers/common/base-mds/build.gradle.kts +++ b/launchers/common/base-mds/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - implementation("logging-house:logging-house-client:0.2.10") + implementation(libs.loggingHouse.client) } val sovityEdcGroup: String by project diff --git a/launchers/common/base/build.gradle.kts b/launchers/common/base/build.gradle.kts index ad391bced..67c78ee33 100644 --- a/launchers/common/base/build.gradle.kts +++ b/launchers/common/base/build.gradle.kts @@ -7,17 +7,17 @@ val edcGroup: String by project dependencies { // Control-Plane - api("${edcGroup}:control-plane-core:${edcVersion}") - api("${edcGroup}:management-api:${edcVersion}") - api("${edcGroup}:api-observability:${edcVersion}") - api("${edcGroup}:configuration-filesystem:${edcVersion}") - api("${edcGroup}:control-plane-aggregate-services:${edcVersion}") - api("${edcGroup}:http:${edcVersion}") - api("${edcGroup}:dsp:${edcVersion}") - api("${edcGroup}:json-ld:${edcVersion}") + api(libs.edc.controlPlaneCore) + api(libs.edc.managementApi) + api(libs.edc.apiObservability) + api(libs.edc.configurationFilesystem) + api(libs.edc.controlPlaneAggregateServices) + api(libs.edc.http) + api(libs.edc.dsp) + api(libs.edc.jsonLd) // Data Management API Key - api("${edcGroup}:auth-tokenbased:${edcVersion}") + api(libs.edc.authTokenbased) // sovity Extensions Package api(project(":extensions:sovity-edc-extensions-package")) @@ -25,15 +25,15 @@ dependencies { api(project(":extensions:transfer-process-status-checker")) // Control-plane to Data-plane - api("${edcGroup}:transfer-data-plane:${edcVersion}") - api("${edcGroup}:data-plane-selector-core:${edcVersion}") - api("${edcGroup}:data-plane-selector-client:${edcVersion}") + api(libs.edc.transferDataPlane) + api(libs.edc.dataPlaneSelectorCore) + api(libs.edc.dataPlaneSelectorClient) // Data-plane - api("${edcGroup}:data-plane-http:${edcVersion}") - api("${edcGroup}:data-plane-framework:${edcVersion}") - api("${edcGroup}:data-plane-core:${edcVersion}") - api("${edcGroup}:data-plane-util:${edcVersion}") + api(libs.edc.dataPlaneHttp) + api(libs.edc.dataPlaneFramework) + api(libs.edc.dataPlaneCore) + api(libs.edc.dataPlaneUtil) } val sovityEdcGroup: String by project diff --git a/launchers/common/observability/build.gradle.kts b/launchers/common/observability/build.gradle.kts index 0622ce5f0..93aca628b 100644 --- a/launchers/common/observability/build.gradle.kts +++ b/launchers/common/observability/build.gradle.kts @@ -7,7 +7,7 @@ val edcGroup: String by project dependencies { // Logging - api("${edcGroup}:monitor-jdk-logger:${edcVersion}") + api(libs.edc.monitorJdkLogger) } val sovityEdcGroup: String by project diff --git a/launchers/connectors/mds-ce/build.gradle.kts b/launchers/connectors/mds-ce/build.gradle.kts index 0779bd143..9ba337461 100644 --- a/launchers/connectors/mds-ce/build.gradle.kts +++ b/launchers/connectors/mds-ce/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` id("application") - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) } val edcVersion: String by project diff --git a/launchers/connectors/sovity-ce/build.gradle.kts b/launchers/connectors/sovity-ce/build.gradle.kts index c871ae810..e0eb6d707 100644 --- a/launchers/connectors/sovity-ce/build.gradle.kts +++ b/launchers/connectors/sovity-ce/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` id("application") - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) } val edcVersion: String by project diff --git a/launchers/connectors/sovity-dev/build.gradle.kts b/launchers/connectors/sovity-dev/build.gradle.kts index e4a27c9a9..df05375fa 100644 --- a/launchers/connectors/sovity-dev/build.gradle.kts +++ b/launchers/connectors/sovity-dev/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` id("application") - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) } dependencies { diff --git a/launchers/connectors/test-backend/build.gradle.kts b/launchers/connectors/test-backend/build.gradle.kts index 82a7d8c29..d2ad0609b 100644 --- a/launchers/connectors/test-backend/build.gradle.kts +++ b/launchers/connectors/test-backend/build.gradle.kts @@ -4,14 +4,14 @@ val edcGroup: String by project plugins { `java-library` id("application") - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) } dependencies { api("${edcGroup}:connector-core:${edcVersion}") - api("${edcGroup}:boot:${edcVersion}") - api("${edcGroup}:http:${edcVersion}") - api("${edcGroup}:api-observability:${edcVersion}") + api(libs.edc.boot) + api(libs.edc.http) + api(libs.edc.apiObservability) api(project(":extensions:test-backend-controller")) } diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 45f2b67a6..1652ef3b8 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -1,6 +1,6 @@ plugins { `java-library` - id("org.gradle.test-retry") version "1.5.7" + alias(libs.plugins.retry) } val assertj: String by project @@ -16,21 +16,19 @@ dependencies { api(project(":launchers:common:base")) api(project(":launchers:common:auth-mock")) - testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") - testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) testImplementation(project(":utils:test-utils")) testImplementation(project(":extensions:test-backend-controller")) testImplementation(project(":utils:test-connector-remote")) testImplementation(project(":extensions:wrapper:clients:java-client")) - testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-params:${jupiterVersion}") - testImplementation("org.mock-server:mockserver-netty:${httpMockServerVersion}") { - // TODO: increase minimum guava version - } - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.jsonUnit.assertj) + testImplementation(libs.mockito.core) + testImplementation(libs.assertj.core) + testImplementation(libs.junit.api) + testImplementation(libs.junit.params) + testImplementation(libs.mockserver.netty) + testRuntimeOnly(libs.junit.engine) } tasks.withType { diff --git a/utils/catalog-parser/build.gradle.kts b/utils/catalog-parser/build.gradle.kts index cdd99b1e4..7e1ca9a98 100644 --- a/utils/catalog-parser/build.gradle.kts +++ b/utils/catalog-parser/build.gradle.kts @@ -12,29 +12,29 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - api("org.glassfish:jakarta.json:${jakartaJsonVersion}") - api("${edcGroup}:core-spi:${edcVersion}") - api("${edcGroup}:control-plane-spi:${edcVersion}") - api("${edcGroup}:json-ld:${edcVersion}") + api(libs.jakarta.json) + api(libs.edc.coreSpi) + api(libs.edc.controlPlaneSpi) + api(libs.edc.jsonLd) implementation(project(":utils:json-and-jsonld-utils")) - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("org.apache.commons:commons-collections4:4.4") - implementation("commons-io:commons-io:2.13.0") + implementation(libs.apache.commonsLang) + implementation(libs.apache.commonsCollections) + implementation(libs.apache.commonsIo) - testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") - testCompileOnly("org.projectlombok:lombok:${lombokVersion}") + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) testImplementation(project(":utils:test-utils")) - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.mockito:mockito-inline:${mockitoVersion}") - testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockito.junitJupiter) + testImplementation(libs.assertj.core) + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } val sovityEdcGroup: String by project diff --git a/utils/json-and-jsonld-utils/build.gradle.kts b/utils/json-and-jsonld-utils/build.gradle.kts index fb5ae7d64..b31e53c36 100644 --- a/utils/json-and-jsonld-utils/build.gradle.kts +++ b/utils/json-and-jsonld-utils/build.gradle.kts @@ -12,24 +12,24 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") - - api("org.glassfish:jakarta.json:${jakartaJsonVersion}") - api("com.apicatalog:titanium-json-ld:1.3.2") - - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("org.apache.commons:commons-collections4:4.4") - implementation("commons-io:commons-io:2.13.0") - - testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") - testCompileOnly("org.projectlombok:lombok:${lombokVersion}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.mockito:mockito-inline:${mockitoVersion}") - testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + api(libs.jakarta.json) + api(libs.apicatalog.titaniumJsonLd) + + implementation(libs.apache.commonsLang) + implementation(libs.apache.commonsCollections) + implementation(libs.apache.commonsIo) + + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockito.junitJupiter) + testImplementation(libs.assertj.core) + testImplementation(libs.junit.api) + testRuntimeOnly(libs.junit.engine) } val sovityEdcGroup: String by project diff --git a/utils/test-connector-remote/build.gradle.kts b/utils/test-connector-remote/build.gradle.kts index 53563830f..0d3e18295 100644 --- a/utils/test-connector-remote/build.gradle.kts +++ b/utils/test-connector-remote/build.gradle.kts @@ -11,23 +11,23 @@ plugins { } dependencies { - annotationProcessor("org.projectlombok:lombok:${lombokVersion}") - compileOnly("org.projectlombok:lombok:${lombokVersion}") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - api("org.junit.jupiter:junit-jupiter-api:5.10.0") - implementation("org.apache.commons:commons-lang3:3.13.0") + api(libs.junit.api) + implementation(libs.apache.commonsLang) - api("${edcGroup}:junit:${edcVersion}") - api("org.awaitility:awaitility:${awaitilityVersion}") + api(libs.edc.junit) + api(libs.awaitility.java) api(project(":utils:json-and-jsonld-utils")) - implementation("${edcGroup}:sql-core:${edcVersion}") - implementation("${edcGroup}:json-ld-spi:${edcVersion}") - implementation("${edcGroup}:json-ld:${edcVersion}") - implementation("org.assertj:assertj-core:${assertj}") - implementation("org.testcontainers:testcontainers:${testcontainersVersion}") - implementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") - implementation("org.testcontainers:postgresql:${testcontainersVersion}") - implementation("io.rest-assured:rest-assured:${restAssured}") + implementation(libs.edc.sqlCore) + implementation(libs.edc.jsonLdSpi) + implementation(libs.edc.jsonLd) + implementation(libs.assertj.core) + implementation(libs.testcontainers.testcontainers) + implementation(libs.testcontainers.junitJupiter) + implementation(libs.testcontainers.postgresql) + implementation(libs.restAssured.restAssured) } val sovityEdcExtensionGroup: String by project From 422371a8ed84ed97f8f9306f129433399a50b93f Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 3 May 2024 11:52:48 +0200 Subject: [PATCH 219/295] chore: Postpone user change to a later stage in this repo merge --- connector/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/connector/Dockerfile b/connector/Dockerfile index 283f08853..f007b5845 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -1,5 +1,8 @@ FROM gradle:7-jdk17-alpine AS build +# TODO https://github.com/sovity/edc-broker-server-extension/issues/425 +USER fixme_and_stop_using_root + ARG USERNAME ARG TOKEN ARG BUILD_ARGS @@ -21,6 +24,9 @@ RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle build --no-daem FROM eclipse-temurin:17-jre-alpine +# TODO https://github.com/sovity/edc-broker-server-extension/issues/425 +USER fixme_and_stop_using_root + # Optional JVM arguments, such as memory settings ARG JVM_ARGS="" From 76e50c182fed5a3c10d35a3d6a66cfc399fb5b1d Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 3 May 2024 14:08:17 +0200 Subject: [PATCH 220/295] chore: Postpone fixing link because of CI --- .github/markdown-link-checker-config.jq | 4 +- SUMMARY.md | 48 ------------------- conflicts/.github/ISSUE_TEMPLATE/release.md | 20 ++++---- conflicts/CHANGELOG.md | 2 +- conflicts/README.md | 2 +- conflicts/STYLEGUIDE.md | 4 +- connector/Dockerfile | 4 +- connector/README.md | 12 ++--- extensions/broker-server-api/api/README.md | 2 +- .../ext/brokerserver/api/ApiInformation.java | 4 +- .../broker-server-api/client-ts/README.md | 2 +- extensions/broker-server-api/client/README.md | 4 +- 12 files changed, 31 insertions(+), 77 deletions(-) delete mode 100644 SUMMARY.md diff --git a/.github/markdown-link-checker-config.jq b/.github/markdown-link-checker-config.jq index 8491117da..5e4e43556 100755 --- a/.github/markdown-link-checker-config.jq +++ b/.github/markdown-link-checker-config.jq @@ -6,7 +6,9 @@ {"pattern": "^https://checkstyle\\.sourceforge\\.io"}, {"pattern": "^https://www\\.linkedin\\.com"}, {"pattern": "https://(.*?)\\.azure\\.sovity\\.io"}, - {"pattern": "http://edc2?:"} + {"pattern": "http://edc2?:"}, + {"pattern": "^https?://broker:"}, + {"pattern": "^https?://connector:"} ], "replacementPatterns": [ { diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index ea8baafc3..000000000 --- a/SUMMARY.md +++ /dev/null @@ -1,48 +0,0 @@ -# Summary - -* [Start](./README.md) -* [Connector Versions](./launchers/README.md) -* [Changelog](./CHANGELOG.md) - - -## User Documentation - -* [Getting Started](./docs/getting-started/README.md) -* [Data Transfer Modes](./docs/getting-started/documentation/data-transfer-methods.md) -* [API Wrapper](./docs/getting-started/documentation/api_wrapper.md) -* [OAuth Data Address](./docs/getting-started/documentation/oauth-data-address.md) -* [Parameterized Assets via UI](./docs/getting-started/documentation/parameterized_assets_via_ui.md) -* [Parameterized Assets via Managment API](./docs/getting-started/documentation/parameterized_assets.md) -* [Pull Data Transfer](./docs/getting-started/documentation/pull-data-transfer.md) - -## Deployment Documentation -* [Deployment Goal: Local Demo](./docs/deployment-guide/goals/local-demo) -* [Deployment Goal: Development](./docs/deployment-guide/goals/development) -* [Deployment Goal: Production](./docs/deployment-guide/goals/production) - * [Productive Deployment Guide](./docs/deployment-guide/goals/production) - * [Productive Deployment Guide 4.2.0 / MS8 / MDS 1.2](docs/deployment-guide/goals/production/4.2.0/README.md) - -## Developer Documentation - -* [Code of Conduct](./CODE_OF_CONDUCT.md) -* [Contribution Guide](./CONTRIBUTING.md) -* [Code-Style Guide](./STYLEGUIDE.md) -* [Security Guide](./SECURITY.md) - - -## Extensions - -* [API Wrapper](./extensions/wrapper/README.md) - * [Community Edition API](./extensions/wrapper/wrapper-api/README.md) - * [Enterprise Edition API](./extensions/wrapper/wrapper-ee-api/README.md) - * [Java API Client Library](./extensions/wrapper/clients/java-client/README.md) - * [Java API Client Library Example](./extensions/wrapper/clients/java-client-example/README.md) - * [TypeScript API Client Library](./extensions/wrapper/clients/typescript-client/README.md) - * [TypeScript API Client Library Example](./extensions/wrapper/clients/typescript-client-example/README.md) -* Policies - * [Always True](./extensions/policy-always-true/README.md) - * [Referring Connector](./extensions/policy-referring-connector/README.md) - * [Time Interval](./extensions/policy-time-interval/README.md) -* [Database Migration](./extensions/postgres-flyway/README.md) -* [EDC UI Config](./extensions/edc-ui-config/README.md) -* [Last Commit Info](./extensions/last-commit-info/README.md) diff --git a/conflicts/.github/ISSUE_TEMPLATE/release.md b/conflicts/.github/ISSUE_TEMPLATE/release.md index 08e3abc83..07fda28e3 100644 --- a/conflicts/.github/ISSUE_TEMPLATE/release.md +++ b/conflicts/.github/ISSUE_TEMPLATE/release.md @@ -26,14 +26,15 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Write or review a release summary. - [ ] Remove empty sections from the patch notes. - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the released edc-extensions version. - - [ ] Set the broker server release version in the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). - - [ ] Set the EDC UI release version in the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). - - [ ] Set the EDC CE release version in the [docker-compose's .env file](https://github.com/sovity/edc-broker-server-extension/blob/main/.env). + - TODO: update links to new .env after the merge + - [ ] Set the broker server release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + - [ ] Set the EDC UI release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + - [ ] Set the EDC CE release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. - [ ] Test the `docker-compose.yaml` with `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main`. - [ ] Create a release and re-use the changelog section as release description, and the version as title. -- [ ] Check if the pipeline built the release versions in the [Actions-Section](https://github.com/sovity/edc-broker-server-extension/actions?query=event%3Arelease) (or you won't see it). +- [ ] Check if the pipeline built the release versions in the [Actions-Section](https://github.com/sovity/edc-extensions/actions?query=event%3Arelease) (or you won't see it). - [ ] Checkout the release tag and check test the `docker-compose.yaml`. - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. - [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. @@ -42,10 +43,11 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Attach the Deployment Docs Zip generated during the GitHub release, which should now contain the CHANGELOG, deployment migration notes, an initial deployment guide and a local demo docker compose. - [ ] Optional, this can be done mid-development if required: - [ ] Create a `release-cleanup` PR. - - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the EDC UI. - - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the EDC CE. - - [ ] Revert the versions in the [docker-compose's .env file](.env) back to latest for the Broker Server. - - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the edc-extensions version `0.0.1-SNAPSHOT`. + - TODO: update links to new .env after the merge + - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the EDC UI. + - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the EDC CE. + - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the Broker Server. + - [ ] Update the [gradle.properties](https://github.com/sovity/edc-extensions/blob/main/gradle.properties) to contain the edc-extensions version `0.0.1-SNAPSHOT`. - [ ] Merge the `release-cleanup` PR. -- [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-broker-server-extension/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. +- [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-extensions/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. - [ ] Close this issue. diff --git a/conflicts/CHANGELOG.md b/conflicts/CHANGELOG.md index 5660d9036..a22c8a468 100644 --- a/conflicts/CHANGELOG.md +++ b/conflicts/CHANGELOG.md @@ -608,7 +608,7 @@ Broker MvP using Core EDC MS8. EDC_BROKER_SERVER_DB_CONNECTION_TIMEOUT_IN_MS: 30000 ``` 3. An issue prevented the keystore file from being read, preventing a successful data space log in. -4. Added a reference to [connector/.env](connector/.env) as source for other possible broker server configuration +4. Added a reference to [connector/.env](.) as source for other possible broker server configuration options, that have defaults, but might have use cases for overriding. #### Compatible Versions diff --git a/conflicts/README.md b/conflicts/README.md index cf6f85a2c..737bff12a 100644 --- a/conflicts/README.md +++ b/conflicts/README.md @@ -192,7 +192,7 @@ EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey ``` All pre-configured config values for either the broker server or the underlying EDC can be found -in [connector/.env](connector/.env). +in [connector/.env](.). TODO: fix url after merge #### UI Configuration diff --git a/conflicts/STYLEGUIDE.md b/conflicts/STYLEGUIDE.md index 400c7edcb..bcc9681c1 100644 --- a/conflicts/STYLEGUIDE.md +++ b/conflicts/STYLEGUIDE.md @@ -3,12 +3,12 @@ In order to maintain a coherent code style throughout the project we ask every contributor to adhere to a few simple style guidelines. We assume most developers will use at least something like `IntelliJ` and therefore have support for automatic code formatting, we are not going to list the guidelines here. If you absolutely want to take a look, checkout -the [config written in XML](resources/checkstyle-config.xml). +the [config written in XML](.) TODO: fix url after merge ## Checkstyle configuration Checkstyle is a [tool](https://checkstyle.sourceforge.io/) that can statically analyze your source code to check against -a set of given rules. Those rules are formulated in an [XML document](resources/checkstyle-config.xml). Many modern +a set of given rules. Those rules are formulated in an [XML document](.) . Many modern IDEs have a plugin available for download that runs in the background and does code analysis. This checkstyle config is based off of the [Google Style](https://checkstyle.sourceforge.io/google_style.html) with a diff --git a/connector/Dockerfile b/connector/Dockerfile index f007b5845..71205d0d6 100644 --- a/connector/Dockerfile +++ b/connector/Dockerfile @@ -1,8 +1,8 @@ -FROM gradle:7-jdk17-alpine AS build - # TODO https://github.com/sovity/edc-broker-server-extension/issues/425 USER fixme_and_stop_using_root +FROM gradle:7-jdk17-alpine AS build + ARG USERNAME ARG TOKEN ARG BUILD_ARGS diff --git a/connector/README.md b/connector/README.md index ee76b6805..d7c86f88d 100644 --- a/connector/README.md +++ b/connector/README.md @@ -16,12 +16,12 @@ ## Image Variants -The Broker Server is built in differnt variants: +The Broker Server is built in different variants: -| Docker Image | Type | Purpose | Features | -|-------------------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| -| [broker-server-dev](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-dev) | Development |
      • Local Deployment via our `docker-compose.yaml`
      • E2E Testing
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | -| [broker-server-ce](https://github.com/sovity/edc-broker-server-extension/pkgs/container/broker-server-ce) | Community Edition |
      • Productive Deployment
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | +| Docker Image | Type | Purpose | Features | +|------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| [broker-server-dev](https://github.com/sovity/edc-extensions/pkgs/container/broker-server-dev) | Development |
      • Local Deployment via our `docker-compose.yaml`
      • E2E Testing
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | +| [broker-server-ce](https://github.com/sovity/edc-extensions/pkgs/container/broker-server-ce) | Community Edition |
      • Productive Deployment
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | ## Image Tags @@ -32,7 +32,7 @@ The Broker Server is built in differnt variants: ## License -Apache License 2.0 - see [LICENSE](../../LICENSE) +Apache License 2.0 - see [LICENSE](../LICENSE) ## Contact diff --git a/extensions/broker-server-api/api/README.md b/extensions/broker-server-api/api/README.md index 22cb75f65..addf0780c 100644 --- a/extensions/broker-server-api/api/README.md +++ b/extensions/broker-server-api/api/README.md @@ -20,7 +20,7 @@ Specification of Broker Server API endpoints, for example endpoints for the Brok ## License -Apache License 2.0 - see [LICENSE](../../../LICENSE.md) +Apache License 2.0 - see [LICENSE](../../../LICENSE) ## Contact diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java index d77ce14ce..6e5f43ae0 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java @@ -28,11 +28,11 @@ contact = @Contact( name = "sovity GmbH", email = "contact@sovity.de", - url = "https://github.com/sovity/edc-broker-server-extension/issues/new/choose" + url = "https://github.com/sovity/edc-extensions/issues/new/choose" ), license = @License( name = "Apache 2.0", - url = "https://github.com/sovity/edc-broker-server-extension/blob/main/LICENSE" + url = "https://github.com/sovity/edc-extensions/blob/main/LICENSE" ) ), externalDocs = @ExternalDocumentation( diff --git a/extensions/broker-server-api/client-ts/README.md b/extensions/broker-server-api/client-ts/README.md index 85741fb43..d76011324 100644 --- a/extensions/broker-server-api/client-ts/README.md +++ b/extensions/broker-server-api/client-ts/README.md @@ -48,7 +48,7 @@ let catalog: CatalogPageResult = await edcClient.brokerServerApi.catalogPage(); ## License Apache License 2.0 - see -[LICENSE](https://github.com/sovity/edc-broker-server-extension/blob/main/LICENSE) +[LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE) ## Contact diff --git a/extensions/broker-server-api/client/README.md b/extensions/broker-server-api/client/README.md index 69b6b616a..1eed30e4f 100644 --- a/extensions/broker-server-api/client/README.md +++ b/extensions/broker-server-api/client/README.md @@ -19,8 +19,6 @@ Java API Client Library to be imported and used in arbitrary applications like use-case backends. -An example project using this client can be found [here](../client-example). - ## Installation ```xml @@ -64,7 +62,7 @@ public class BrokerServerClientExample { ## License -Apache License 2.0 - see [LICENSE](../../LICENSE) +Apache License 2.0 - see [LICENSE](../../../LICENSE) ## Contact From 3cddd0625b3fe27de10319122e07da08e52b9eee Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 3 May 2024 15:19:16 +0200 Subject: [PATCH 221/295] fix: Run CI on integration branches --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6215677c..68abaac9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: release: types: [ published ] pull_request: - branches: [ main ] + branches: [ main, "integration/*" ] env: REGISTRY_URL: ghcr.io From 938ec7d23932ce31f5cefb3139887db00ba9c74d Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Mon, 6 May 2024 12:29:40 +0200 Subject: [PATCH 222/295] deps: bump postgres lib (#922) --- CHANGELOG.md | 2 ++ gradle/libs.versions.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a157402dd..dcbd34e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes +Security updates + ### Deployment Migration Notes ## [7.4.2] - 2024-04-20 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca6376cf0..115bb1b81 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ okhttp = "4.11.0" okio = "3.9.0" openapiGenerator = "6.6.0" openapiJackson = "0.2.6" -postgres = "42.4.0" +postgres = "42.4.5" quarkus = "2.16.6.Final" restAssured = "4.5.0" retry = "1.5.7" From 442a1d8cce8f1a913f78d319e4b74032577d0aa1 Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Thu, 16 May 2024 17:00:51 +0200 Subject: [PATCH 223/295] API Wrapper: filter expressions on catalog fetch (#923) * feat: implemented use case catalog fetch with filterExpressions --- CHANGELOG.md | 2 + docs/api/sovity-edc-api-wrapper.yaml | 87 ++++++++ .../wrapper/api/usecase/UseCaseResource.java | 16 ++ .../model/CatalogFilterExpression.java | 36 +++ .../model/CatalogFilterExpressionLiteral.java | 50 +++++ .../CatalogFilterExpressionLiteralType.java | 22 ++ .../CatalogFilterExpressionOperator.java | 26 +++ .../api/usecase/model/CatalogQuery.java | 40 ++++ .../WrapperExtensionContextBuilder.java | 32 ++- .../ext/wrapper/api/ui/UiResourceImpl.java | 2 +- .../ui/pages/catalog/CatalogApiService.java | 47 +--- .../ui/pages/catalog/UiDataOfferBuilder.java | 73 +++++++ .../ContractAgreementTransferApiService.java | 2 +- .../api/usecase/UseCaseResourceImpl.java | 9 + .../FilterExpressionLiteralMapper.java | 50 +++++ .../pages/catalog/FilterExpressionMapper.java | 41 ++++ .../FilterExpressionOperatorMapper.java | 49 +++++ .../catalog/UseCaseCatalogApiService.java | 55 +++++ .../api/ui/pages/catalog/CatalogApiTest.java | 1 - .../services/TransferRequestBuilderTest.java | 2 +- .../api/usecase/UseCaseApiWrapperTest.java | 188 ++++++++++++++++ .../sovity/edc/e2e/UseCaseApiWrapperTest.java | 206 ++++++++++++++++++ .../edc/utils/catalog/DspCatalogService.java | 15 +- 23 files changed, 993 insertions(+), 58 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteralType.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/UiDataOfferBuilder.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionLiteralMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionOperatorMapper.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/UseCaseCatalogApiService.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dcbd34e17..1dc497c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- API Wrapper Use Case API: Catalog endpoint + #### Patch Changes Security updates diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 2952736aa..15670f25f 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -436,6 +436,27 @@ paths: type: array items: type: string + /wrapper/use-case-api/catalog: + post: + tags: + - Use Case + description: Fetch a connector's data offers + operationId: queryCatalog + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogQuery' + required: true + responses: + default: + description: default response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UiDataOffer' components: schemas: ConnectorLimits: @@ -1575,3 +1596,69 @@ components: format: int64 description: States and counts of outgoing transferprocess counts description: Counts of incoming and outgoing TransferProcesses and status + CatalogFilterExpression: + required: + - operandLeft + - operandRight + - operator + type: object + properties: + operandLeft: + type: string + description: Asset property name + example: https://w3id.org/edc/v0.0.1/ns/assetId + operator: + $ref: '#/components/schemas/CatalogFilterExpressionOperator' + operandRight: + $ref: '#/components/schemas/CatalogFilterExpressionLiteral' + description: Generic expression for filtering the data offers in the catalog + CatalogFilterExpressionLiteral: + type: object + properties: + type: + $ref: '#/components/schemas/CatalogFilterExpressionLiteralType' + value: + type: string + description: Only for type VALUE. The single value representation. + valueList: + type: array + description: "Only for type VALUE_LIST. List of values, e.g. for the IN-Operator." + items: + type: string + description: "Only for type VALUE_LIST. List of values, e.g. for the IN-Operator." + description: FilterExpression Criterion Literal + CatalogFilterExpressionLiteralType: + type: string + description: Value type of a filter expression criterion + enum: + - VALUE + - VALUE_LIST + CatalogFilterExpressionOperator: + type: string + description: Operator for filter expressions + enum: + - LIKE + - EQ + - IN + CatalogQuery: + required: + - connectorEndpoint + type: object + properties: + connectorEndpoint: + type: string + description: Target EDC DSP endpoint URL + limit: + type: integer + description: Limit the number of results + format: int32 + offset: + type: integer + description: "Offset for returned results, e.g. start at result 2" + format: int32 + filterExpressions: + type: array + description: Filter expressions for catalog filtering + items: + $ref: '#/components/schemas/CatalogFilterExpression' + description: Catalog query parameters diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java index 9c6dba153..988f89bef 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -14,10 +14,16 @@ package de.sovity.edc.ext.wrapper.api.usecase; +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @@ -43,4 +49,14 @@ public interface UseCaseResource { @Produces(MediaType.APPLICATION_JSON) @Operation(description = "List available functions in policies, prohibitions and obligations.") List getSupportedFunctions(); + + @POST + @Path("catalog") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Fetch a connector's data offers") + List queryCatalog( + @Valid @NotNull + CatalogQuery catalogQuery + ); } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java new file mode 100644 index 000000000..a1f0864a4 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Generic expression for filtering the data offers in the catalog", requiredMode = Schema.RequiredMode.NOT_REQUIRED) +public class CatalogFilterExpression { + @Schema(description = "Asset property name", requiredMode = Schema.RequiredMode.REQUIRED, example = Prop.Edc.ASSET_ID) + private String operandLeft; + + @Schema(description = "Operator", requiredMode = Schema.RequiredMode.REQUIRED) + private CatalogFilterExpressionOperator operator; + + @Schema(description = "Right Operand", requiredMode = Schema.RequiredMode.REQUIRED) + private CatalogFilterExpressionLiteral operandRight; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java new file mode 100644 index 000000000..e04a0ec42 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +@Schema(description = "FilterExpression Criterion Literal") +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CatalogFilterExpressionLiteral { + + private CatalogFilterExpressionLiteralType type; + + @Schema(description = "Only for type VALUE. The single value representation.") + private String value; + + @Schema(description = "Only for type VALUE_LIST. List of values, e.g. for the IN-Operator.") + private List valueList; + + public static CatalogFilterExpressionLiteral ofValue(@NonNull String value) { + return new CatalogFilterExpressionLiteral(CatalogFilterExpressionLiteralType.VALUE, value, null); + } + + public static CatalogFilterExpressionLiteral ofValueList(@NonNull List valueList) { + return new CatalogFilterExpressionLiteral(CatalogFilterExpressionLiteralType.VALUE_LIST, null, valueList); + } +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteralType.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteralType.java new file mode 100644 index 000000000..f0077f606 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteralType.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Value type of a filter expression criterion", enumAsRef = true) +public enum CatalogFilterExpressionLiteralType { + VALUE, VALUE_LIST +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java new file mode 100644 index 000000000..a098c3ae4 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(description = "Operator for filter expressions", enumAsRef = true) +public enum CatalogFilterExpressionOperator { + LIKE, EQ, IN +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java new file mode 100644 index 000000000..6ce7d90db --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Catalog query parameters") +public class CatalogQuery { + @Schema(description = "Target EDC DSP endpoint URL", requiredMode = Schema.RequiredMode.REQUIRED) + private String connectorEndpoint; + + @Schema(description = "Limit the number of results", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Integer limit; + + @Schema(description = "Offset for returned results, e.g. start at result 2", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Integer offset; + + @Schema(description = "Filter expressions for catalog filtering", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List filterExpressions; +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 4721d5a3f..8a87a6110 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -33,6 +33,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder; import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetIdValidator; import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.CatalogApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.UiDataOfferBuilder; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTransferApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementDataFetcher; @@ -60,6 +61,10 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; import de.sovity.edc.ext.wrapper.api.usecase.UseCaseResourceImpl; +import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.FilterExpressionLiteralMapper; +import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.FilterExpressionMapper; +import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.FilterExpressionOperatorMapper; +import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; import de.sovity.edc.utils.catalog.DspCatalogService; @@ -137,7 +142,13 @@ public static WrapperExtensionContext buildContext( var textUtils = new TextUtils(); var selfDescriptionService = new SelfDescriptionService(config, monitor); var ownConnectorEndpointService = new OwnConnectorEndpointServiceImpl(selfDescriptionService); - var uiAssetMapper = new UiAssetMapper(edcPropertyUtils, assetJsonLdUtils, markdownToTextConverter, textUtils, ownConnectorEndpointService); + var uiAssetMapper = new UiAssetMapper( + edcPropertyUtils, + assetJsonLdUtils, + markdownToTextConverter, + textUtils, + ownConnectorEndpointService + ); var assetMapper = new AssetMapper(typeTransformerRegistry, uiAssetMapper, jsonLd); var transferProcessStateService = new TransferProcessStateService(); var contractNegotiationUtils = new ContractNegotiationUtils( @@ -210,10 +221,10 @@ public static WrapperExtensionContext buildContext( policyMapper ); var dataOfferBuilder = new DspDataOfferBuilder(jsonLd); + var uiDataOfferBuilder = new UiDataOfferBuilder(assetMapper, policyMapper); var dspCatalogService = new DspCatalogService(catalogService, dataOfferBuilder); var catalogApiService = new CatalogApiService( - assetMapper, - policyMapper, + uiDataOfferBuilder, dspCatalogService ); var contractOfferMapper = new ContractOfferMapper(policyMapper); @@ -254,6 +265,13 @@ public static WrapperExtensionContext buildContext( ); // Use Case API + var filterExpressionOperatorMapper = new FilterExpressionOperatorMapper(); + var filterExpressionLiteralMapper = new FilterExpressionLiteralMapper(); + var filterExpressionMapper = new FilterExpressionMapper( + filterExpressionOperatorMapper, + filterExpressionLiteralMapper + ); + var kpiApiService = new KpiApiService( assetIndex, policyDefinitionStore, @@ -263,9 +281,15 @@ public static WrapperExtensionContext buildContext( transferProcessStateService ); var supportedPolicyApiService = new SupportedPolicyApiService(policyEngine); + var useCaseCatalogApiService = new UseCaseCatalogApiService( + uiDataOfferBuilder, + dspCatalogService, + filterExpressionMapper + ); var useCaseResource = new UseCaseResourceImpl( kpiApiService, - supportedPolicyApiService + supportedPolicyApiService, + useCaseCatalogApiService ); // Collect all JAX-RS resources diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index aad1111ac..9e9aed244 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -20,12 +20,12 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; -import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.TransferHistoryPage; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java index c5d27f601..05fc378b2 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiService.java @@ -14,62 +14,19 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.catalog; -import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; -import de.sovity.edc.ext.wrapper.api.ui.model.UiContractOffer; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import de.sovity.edc.utils.catalog.DspCatalogService; -import de.sovity.edc.utils.catalog.model.DspContractOffer; -import de.sovity.edc.utils.catalog.model.DspDataOffer; import lombok.RequiredArgsConstructor; import java.util.List; @RequiredArgsConstructor public class CatalogApiService { - private final AssetMapper assetMapper; - private final PolicyMapper policyMapper; + private final UiDataOfferBuilder uiDataOfferBuilder; private final DspCatalogService dspCatalogService; public List fetchDataOffers(String connectorEndpoint) { var dspCatalog = dspCatalogService.fetchDataOffers(connectorEndpoint); - var endpoint = dspCatalog.getEndpoint(); - var participantId = dspCatalog.getParticipantId(); - - return dspCatalog.getDataOffers().stream() - .map(dataOffer -> buildDataOffer(dataOffer, endpoint, participantId)) - .toList(); - } - - private UiDataOffer buildDataOffer(DspDataOffer dataOffer, String endpoint, String participantId) { - var uiDataOffer = new UiDataOffer(); - uiDataOffer.setEndpoint(endpoint); - uiDataOffer.setParticipantId(participantId); - uiDataOffer.setAsset(buildUiAsset(dataOffer, endpoint, participantId)); - uiDataOffer.setContractOffers(buildContractOffers(dataOffer.getContractOffers())); - return uiDataOffer; - } - - private List buildContractOffers(List contractOffers) { - return contractOffers.stream().map(this::buildContractOffer).toList(); - } - - private UiContractOffer buildContractOffer(DspContractOffer contractOffer) { - var uiContractOffer = new UiContractOffer(); - uiContractOffer.setContractOfferId(contractOffer.getContractOfferId()); - uiContractOffer.setPolicy(buildUiPolicy(contractOffer)); - return uiContractOffer; - } - - private UiAsset buildUiAsset(DspDataOffer dataOffer, String endpoint, String participantId) { - var asset = assetMapper.buildAssetFromDatasetProperties(dataOffer.getAssetPropertiesJsonLd()); - return assetMapper.buildUiAsset(asset, endpoint, participantId); - } - - private UiPolicy buildUiPolicy(DspContractOffer contractOffer) { - var policy = policyMapper.buildPolicy(contractOffer.getPolicyJsonLd()); - return policyMapper.buildUiPolicy(policy); + return uiDataOfferBuilder.buildUiDataOffers(dspCatalog); } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/UiDataOfferBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/UiDataOfferBuilder.java new file mode 100644 index 000000000..5132848ff --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/UiDataOfferBuilder.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; +import de.sovity.edc.ext.wrapper.api.ui.model.UiContractOffer; +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.utils.catalog.model.DspCatalog; +import de.sovity.edc.utils.catalog.model.DspContractOffer; +import de.sovity.edc.utils.catalog.model.DspDataOffer; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class UiDataOfferBuilder { + private final AssetMapper assetMapper; + private final PolicyMapper policyMapper; + + public List buildUiDataOffers(DspCatalog dspCatalog) { + var endpoint = dspCatalog.getEndpoint(); + var participantId = dspCatalog.getParticipantId(); + + return dspCatalog.getDataOffers().stream() + .map(dataOffer -> buildDataOffer(dataOffer, endpoint, participantId)) + .toList(); + } + + private UiDataOffer buildDataOffer(DspDataOffer dataOffer, String endpoint, String participantId) { + var uiDataOffer = new UiDataOffer(); + uiDataOffer.setEndpoint(endpoint); + uiDataOffer.setParticipantId(participantId); + uiDataOffer.setAsset(buildUiAsset(dataOffer, endpoint, participantId)); + uiDataOffer.setContractOffers(buildContractOffers(dataOffer.getContractOffers())); + return uiDataOffer; + } + + private List buildContractOffers(List contractOffers) { + return contractOffers.stream().map(this::buildContractOffer).toList(); + } + + private UiContractOffer buildContractOffer(DspContractOffer contractOffer) { + var uiContractOffer = new UiContractOffer(); + uiContractOffer.setContractOfferId(contractOffer.getContractOfferId()); + uiContractOffer.setPolicy(buildUiPolicy(contractOffer)); + return uiContractOffer; + } + + private UiAsset buildUiAsset(DspDataOffer dataOffer, String endpoint, String participantId) { + var asset = assetMapper.buildAssetFromDatasetProperties(dataOffer.getAssetPropertiesJsonLd()); + return assetMapper.buildUiAsset(asset, endpoint, participantId); + } + + private UiPolicy buildUiPolicy(DspContractOffer contractOffer) { + var policy = policyMapper.buildPolicy(contractOffer.getPolicyJsonLd()); + return policyMapper.buildUiPolicy(policy); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java index 3f49bd642..a839f3180 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTransferApiService.java @@ -14,8 +14,8 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements; -import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.TransferRequestBuilder; import lombok.RequiredArgsConstructor; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java index 8418ea75f..7f1bf7d74 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java @@ -14,7 +14,10 @@ package de.sovity.edc.ext.wrapper.api.usecase; +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; +import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; import lombok.RequiredArgsConstructor; @@ -29,6 +32,7 @@ public class UseCaseResourceImpl implements UseCaseResource { private final KpiApiService kpiApiService; private final SupportedPolicyApiService supportedPolicyApiService; + private final UseCaseCatalogApiService useCaseCatalogApiService; @Override public KpiResult getKpis() { @@ -39,4 +43,9 @@ public KpiResult getKpis() { public List getSupportedFunctions() { return supportedPolicyApiService.getSupportedFunctions(); } + + @Override + public List queryCatalog(CatalogQuery catalogQuery) { + return useCaseCatalogApiService.fetchDataOffers(catalogQuery); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionLiteralMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionLiteralMapper.java new file mode 100644 index 000000000..9332805d1 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionLiteralMapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteral; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteralType; +import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogFilterExpressionLiteral; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class FilterExpressionLiteralMapper { + public UiCriterionLiteral buildUiCriterionLiteral(Object value) { + if (value instanceof Collection) { + var list = getValueList((Collection) value); + return UiCriterionLiteral.ofValueList(list); + } + + return UiCriterionLiteral.ofValue(value.toString()); + } + + public Object getValue(CatalogFilterExpressionLiteral dto) { + return switch (dto.getType()) { + case VALUE -> dto.getValue(); + case VALUE_LIST -> dto.getValueList(); + default -> throw new IllegalStateException("Unhandled %s: %s".formatted( + UiCriterionLiteralType.class.getName(), + dto.getType() + )); + }; + } + + private List getValueList(Collection valueList) { + return valueList.stream().map(it -> it == null ? null : it.toString()).toList(); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionMapper.java new file mode 100644 index 000000000..b369e973e --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogFilterExpression; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.query.Criterion; + +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +public class FilterExpressionMapper { + private final FilterExpressionOperatorMapper filterExpressionOperatorMapper; + private final FilterExpressionLiteralMapper filterExpressionLiteralMapper; + + public List buildCriteria(@NonNull List filterExpressions) { + return filterExpressions.stream().filter(Objects::nonNull).map(this::buildCriterion).toList(); + } + + public Criterion buildCriterion(@NonNull CatalogFilterExpression filterExpression) { + var operandLeft = filterExpression.getOperandLeft(); + var operator = filterExpressionOperatorMapper.getCriterionOperator(filterExpression.getOperator()); + var operandRight = filterExpressionLiteralMapper.getValue(filterExpression.getOperandRight()); + + return new Criterion(operandLeft, operator, operandRight); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionOperatorMapper.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionOperatorMapper.java new file mode 100644 index 000000000..5712d38f4 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/FilterExpressionOperatorMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogFilterExpressionOperator; +import de.sovity.edc.ext.wrapper.utils.MapUtils; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +@RequiredArgsConstructor +public class FilterExpressionOperatorMapper { + /** + * @see org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl + */ + private final Map mappings = Map.of( + CatalogFilterExpressionOperator.EQ, "=", + CatalogFilterExpressionOperator.LIKE, "like", + CatalogFilterExpressionOperator.IN, "in" + ); + + private final Map reverseMappings = MapUtils.reverse(mappings); + + public String getCriterionOperator(CatalogFilterExpressionOperator operator) { + String result = mappings.get(operator); + return requireNonNull(result, () -> "Unhandled %s: %s".formatted( + CatalogFilterExpressionOperator.class.getName(), operator)); + } + + public CatalogFilterExpressionOperator getCatalogFilterExpressionOperator(String operator) { + CatalogFilterExpressionOperator result = reverseMappings.get(operator == null ? null : operator.toLowerCase()); + return requireNonNull(result, () -> "Could not find %s for: %s".formatted( + CatalogFilterExpressionOperator.class.getName(), operator)); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/UseCaseCatalogApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/UseCaseCatalogApiService.java new file mode 100644 index 000000000..49da44363 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/pages/catalog/UseCaseCatalogApiService.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.pages.catalog; + +import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.UiDataOfferBuilder; +import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; +import de.sovity.edc.utils.catalog.DspCatalogService; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.util.List; + +@RequiredArgsConstructor +public class UseCaseCatalogApiService { + private final UiDataOfferBuilder uiDataOfferBuilder; + private final DspCatalogService dspCatalogService; + private final FilterExpressionMapper filterExpressionMapper; + + public List fetchDataOffers(CatalogQuery catalogQuery) { + var querySpec = buildQuerySpec(catalogQuery); + + var dspCatalog = dspCatalogService.fetchDataOffersWithFilters(catalogQuery.getConnectorEndpoint(), querySpec); + return uiDataOfferBuilder.buildUiDataOffers(dspCatalog); + } + + private QuerySpec buildQuerySpec(CatalogQuery params) { + var builder = QuerySpec.Builder.newInstance() + .limit(withDefault(params.getLimit(), Integer.MAX_VALUE)) + .offset(withDefault(params.getOffset(), 0)); + + var filterExpressions = params.getFilterExpressions(); + if (filterExpressions != null && !filterExpressions.isEmpty()) { + builder.filter(filterExpressionMapper.buildCriteria(filterExpressions)); + } + + return builder.build(); + } + + private T withDefault(T valueOrNull, T orElse) { + return valueOrNull == null ? orElse : valueOrNull; + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java index 9030a3ea2..bf32c520c 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java @@ -16,7 +16,6 @@ import org.eclipse.edc.junit.extensions.EdcExtension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.api.extension.ExtendWith; import java.util.List; diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java index 01a36294a..a29186b0a 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java @@ -14,9 +14,9 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; +import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; import lombok.val; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java new file mode 100644 index 000000000..04860b888 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java @@ -0,0 +1,188 @@ +package de.sovity.edc.ext.wrapper.api.usecase; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.CatalogFilterExpression; +import de.sovity.edc.client.gen.model.CatalogFilterExpressionLiteral; +import de.sovity.edc.client.gen.model.CatalogFilterExpressionLiteralType; +import de.sovity.edc.client.gen.model.CatalogFilterExpressionOperator; +import de.sovity.edc.client.gen.model.CatalogQuery; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApiTest +@ExtendWith(EdcExtension.class) +public class UseCaseApiWrapperTest { + private EdcClient client; + + private String assetId1 = "test-asset-1"; + private String assetId2 = "test-asset-2"; + private String policyId = "policy-1"; + + + @BeforeEach + void setup(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + @DisabledOnGithub + void shouldFetchFilteredDataOffersWithEq() { + // arrange + setupAssets(); + buildContractDefinition(policyId, assetId1, "cd-1"); + buildContractDefinition(policyId, assetId2, "cd-2"); + + // act + var catalogQueryParamsEq = criterion(Prop.Edc.ID, CatalogFilterExpressionOperator.EQ, "test-asset-1"); + var dataOffers = client.useCaseApi().queryCatalog(catalogQueryParamsEq); + + // assert + assertThat(dataOffers).hasSize(1); + assertThat(dataOffers.get(0).getAsset().getAssetId()).isEqualTo(assetId1); + assertThat(dataOffers.get(0).getAsset().getTitle()).isEqualTo("Test Asset 1"); + + } + + @Test + @DisabledOnGithub + void shouldFetchFilteredDataOffersWithIn() { + // arrange + setupAssets(); + buildContractDefinition(policyId, assetId1, "cd-1"); + buildContractDefinition(policyId, assetId2, "cd-2"); + + // act + var catalogQueryParamsEq = criterion(Prop.Edc.ID, CatalogFilterExpressionOperator.IN, List.of("test-asset-1", "test-asset-2")); + var dataOffers = client.useCaseApi().queryCatalog(catalogQueryParamsEq); + + // assert + assertThat(dataOffers).hasSize(2); + assertThat(dataOffers) + .extracting(it -> it.getAsset().getAssetId()) + .containsExactlyInAnyOrder(assetId1, assetId2); + } + + @Test + @DisabledOnGithub + void shouldFetchWithoutFilterButWithLimit() { + // arrange + setupAssets(); + buildContractDefinition(policyId, assetId1, "cd-1"); + buildContractDefinition(policyId, assetId2, "cd-2"); + + // act + var catalogQueryParamsEq = criterion(1, 0); + var dataOffers = client.useCaseApi().queryCatalog(catalogQueryParamsEq); + + // assert + assertThat(dataOffers).hasSize(1); + assertThat(dataOffers) + .extracting(it -> it.getAsset().getAssetId()) + .containsAnyOf(assetId1, assetId2); + } + + private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperator operator, String rightOperand) { + return CatalogQuery.builder() + .connectorEndpoint(TestUtils.PROTOCOL_ENDPOINT) + .filterExpressions( + List.of( + CatalogFilterExpression.builder() + .operandLeft(leftOperand) + .operator(operator) + .operandRight(CatalogFilterExpressionLiteral.builder().value(rightOperand).type(CatalogFilterExpressionLiteralType.VALUE).build()) + .build() + ) + ) + .build(); + } + + private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperator operator, List rightOperand) { + return CatalogQuery.builder() + .connectorEndpoint(TestUtils.PROTOCOL_ENDPOINT) + .filterExpressions( + List.of( + CatalogFilterExpression.builder() + .operandLeft(leftOperand) + .operator(operator) + .operandRight(CatalogFilterExpressionLiteral.builder().valueList(rightOperand).type(CatalogFilterExpressionLiteralType.VALUE_LIST).build()) + .build() + ) + ) + .build(); + } + + private CatalogQuery criterion(Integer limit, Integer offset) { + return CatalogQuery.builder() + .connectorEndpoint(TestUtils.PROTOCOL_ENDPOINT) + .limit(limit) + .offset(offset) + .build(); + } + + private void buildContractDefinition(String policyId, String assetId1, String cdId) { + client.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId(cdId) + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId1) + .build()) + .build())) + .build()); + } + + private void setupAssets() { + assetId1 = client.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("test-asset-1") + .title("Test Asset 1") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, TestUtils.PROTOCOL_ENDPOINT + )) + .mediaType("application/json") + .build()).getId(); + + assetId2 = client.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("test-asset-2") + .title("Test Asset 2") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, TestUtils.PROTOCOL_ENDPOINT + )) + .mediaType("application/json") + .build()).getId(); + + policyId = client.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() + .policyDefinitionId("policy-1") + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build()).getId(); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java new file mode 100644 index 000000000..bcc18e5c4 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.CatalogFilterExpression; +import de.sovity.edc.client.gen.model.CatalogFilterExpressionLiteral; +import de.sovity.edc.client.gen.model.CatalogFilterExpressionLiteralType; +import de.sovity.edc.client.gen.model.CatalogFilterExpressionOperator; +import de.sovity.edc.client.gen.model.CatalogQuery; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiPolicyConstraint; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyLiteral; +import de.sovity.edc.client.gen.model.UiPolicyLiteralType; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static org.assertj.core.api.Assertions.assertThat; + +class UseCaseApiWrapperTest { + + private static final String PROVIDER_PARTICIPANT_ID = "provider"; + private static final String CONSUMER_PARTICIPANT_ID = "consumer"; + + @RegisterExtension + static EdcExtension providerEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension consumerEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorRemote providerConnector; + private ConnectorRemote consumerConnector; + + private EdcClient providerClient; + private EdcClient consumerClient; + private MockDataAddressRemote dataAddress; + private final String dataOfferData = "expected data 123"; + + private final String dataOfferId = "my-data-offer-2023-11"; + + @BeforeEach + void setup() { + // set up provider EDC + Client + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + providerEdcContext.setConfiguration(providerConfig.getProperties()); + providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // set up consumer EDC + Client + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + consumerEdcContext.setConfiguration(consumerConfig.getProperties()); + consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) + dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + } + + @Test + void catalog_filtering_by_like() { + // arrange + createPolicy(); + createAsset(); + createContractDefinition(); + + var query = criterion(Prop.Edc.ID, CatalogFilterExpressionOperator.LIKE, "%data-offer%"); + + // act + var dataOffers = consumerClient.useCaseApi().queryCatalog(query); + + // assert + assertThat(dataOffers).hasSize(1); + assertThat(dataOffers.get(0).getAsset().getAssetId()).isEqualTo(dataOfferId); + assertThat(dataOffers.get(0).getAsset().getTitle()).isEqualTo("My Data Offer"); + + } + + private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperator operator, String rightOperand) { + return CatalogQuery.builder() + .connectorEndpoint(getProtocolEndpoint(providerConnector)) + .filterExpressions( + List.of( + CatalogFilterExpression.builder() + .operandLeft(leftOperand) + .operator(operator) + .operandRight(CatalogFilterExpressionLiteral.builder().value(rightOperand).type(CatalogFilterExpressionLiteralType.VALUE).build()) + .build() + ) + ) + .build(); + } + + private void createAsset() { + var asset = UiAssetCreateRequest.builder() + .id(dataOfferId) + .title("My Data Offer") + .description("Example Data Offer.") + .version("2023-11") + .language("EN") + .publisherHomepage("https://my-department.my-org.com/my-data-offer") + .licenseUrl("https://my-department.my-org.com/my-data-offer#license") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(dataOfferData) + )) + .build(); + + providerClient.uiApi().createAsset(asset); + } + + private void createPolicy() { + var afterYesterday = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().minusDays(1).toString()) + .build()) + .build(); + + var beforeTomorrow = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.LT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().plusDays(1).toString()) + .build()) + .build(); + + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(dataOfferId) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(afterYesterday, beforeTomorrow)) + .build()) + .build(); + + providerClient.uiApi().createPolicyDefinition(policyDefinition); + } + + private void createContractDefinition() { + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); + + providerClient.uiApi().createContractDefinition(contractDefinition); + } + + private String getProtocolEndpoint(ConnectorRemote connector) { + return connector.getConfig().getProtocolEndpoint().getUri().toString(); + } +} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java index a31d5461a..e9559cf2a 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java @@ -30,20 +30,25 @@ public class DspCatalogService { private final DspDataOfferBuilder dspDataOfferBuilder; public DspCatalog fetchDataOffers(String endpoint) throws DspCatalogServiceException { - var catalogJson = fetchDcatResponse(endpoint); + var catalogJson = fetchDcatResponse(endpoint, QuerySpec.max()); return dspDataOfferBuilder.buildDataOffers(endpoint, catalogJson); } - private JsonObject fetchDcatResponse(String connectorEndpoint) { - var raw = fetchDcatRaw(connectorEndpoint); + public DspCatalog fetchDataOffersWithFilters(String endpoint, QuerySpec querySpec) { + var catalogJson = fetchDcatResponse(endpoint, querySpec); + return dspDataOfferBuilder.buildDataOffers(endpoint, catalogJson); + } + + private JsonObject fetchDcatResponse(String connectorEndpoint, QuerySpec querySpec) { + var raw = fetchDcatRaw(connectorEndpoint, querySpec); var string = new String(raw, StandardCharsets.UTF_8); return JsonUtils.parseJsonObj(string); } @SneakyThrows - private byte[] fetchDcatRaw(String connectorEndpoint) { + private byte[] fetchDcatRaw(String connectorEndpoint, QuerySpec querySpec) { return catalogService - .requestCatalog(connectorEndpoint, "dataspace-protocol-http", QuerySpec.max()) + .requestCatalog(connectorEndpoint, "dataspace-protocol-http", querySpec) .get() .orElseThrow(DspCatalogServiceException::ofFailure); } From 2a439f7f904b768c2a7066133a8bf29d2f994dbb Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 16 May 2024 18:01:18 +0200 Subject: [PATCH 224/295] Release prep 7.5.0 (#925) * chore: v7.4.3 release preparation * Disable flaky test --- .env | 4 +-- CHANGELOG.md | 27 +++++++++++++++++++ .../sovity/edc/e2e/UseCaseApiWrapperTest.java | 2 ++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.env b/.env index a0d3c036f..3afd44a69 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.4.2 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:7.4.2 +EDC_IMAGE=ghcr.io/sovity/edc-dev:7.5.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:7.5.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc497c9b..9e7690898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,24 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +#### Patch Changes + +### Deployment Migration Notes + +## [7.5.0] - 2024-05-16 + +### Overview + +Additional Wrapper API features + +### EDC UI + +- https://github.com/sovity/edc-ui/releases/tag/v3.2.2 + +### EDC Extensions + +#### Minor Changes + - API Wrapper Use Case API: Catalog endpoint #### Patch Changes @@ -23,6 +41,15 @@ Security updates ### Deployment Migration Notes +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:7.5.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.5.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.5.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` +- Connector UI Release: https://github.com/sovity/edc-ui/releases/tag/v3.2.2 + ## [7.4.2] - 2024-04-20 ### Overview diff --git a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java index bcc18e5c4..e6df6d88a 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java @@ -35,6 +35,7 @@ import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; import de.sovity.edc.extension.e2e.db.TestDatabase; import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.jsonld.vocab.Prop; import org.eclipse.edc.junit.extensions.EdcExtension; import org.junit.jupiter.api.BeforeEach; @@ -100,6 +101,7 @@ void setup() { dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); } + @DisabledOnGithub @Test void catalog_filtering_by_like() { // arrange From 1133b2ad11ec646c91964e7dee5ea35f8e4a8d95 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 17 May 2024 12:41:45 +0200 Subject: [PATCH 225/295] ci: speed-up documenation PRs (#926) --- .github/ISSUE_TEMPLATE/release.md | 1 + .github/workflows/check_docs.yml | 24 ++++++++++++++++++++++++ .github/workflows/ci.yml | 18 +++++------------- 3 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/check_docs.yml diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 6ff21b07a..0dc38085d 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -51,6 +51,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] If the Eclipse EDC version changed, update the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-extensions/blob/main/docs/api/eclipse-edc-management-api.yaml). - [ ] Update the Postman Collection if required. + - [ ] Run all tests locally as long as the [GH flaky tests](https://github.com/sovity/edc-extensions/issues/870) are a problem. - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-extensions/actions). - [ ] Validate the image diff --git a/.github/workflows/check_docs.yml b/.github/workflows/check_docs.yml new file mode 100644 index 000000000..b16f1266e --- /dev/null +++ b/.github/workflows/check_docs.yml @@ -0,0 +1,24 @@ +name: Documentation Checks + +on: + push: + branches: [ main ] + release: + types: [ published ] + pull_request: + branches: [ main ] + +jobs: + markdown-link-checks: + name: Markdown Link Checks + runs-on: ubuntu-latest + steps: + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: actions/checkout@master + - name: "Markdown Link Checker: Generate Config" + run: .github/markdown-link-checker-config.jq > .github/markdown-link-checker-config.json + - name: "Markdown Link Checker: Validate Links" + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-quiet-mode: 'yes' + config-file: '.github/markdown-link-checker-config.json' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6215677c..fe921bdb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,11 @@ on: types: [ published ] pull_request: branches: [ main ] + paths-ignore: + - '**.md' + - '**.png' + - 'docs/api/**' + - 'LICENSE' env: REGISTRY_URL: ghcr.io @@ -153,16 +158,3 @@ jobs: env: NODE_USER: richardtreier-sovity NODE_AUTH_TOKEN: ${{ secrets.SOVITY_EDC_CLIENT_NPM_AUTH }} - markdown-link-checks: - name: Markdown Link Checks - runs-on: ubuntu-latest - steps: - - uses: FranzDiebold/github-env-vars-action@v2 - - uses: actions/checkout@master - - name: "Markdown Link Checker: Generate Config" - run: .github/markdown-link-checker-config.jq > .github/markdown-link-checker-config.json - - name: "Markdown Link Checker: Validate Links" - uses: gaurav-nelson/github-action-markdown-link-check@v1 - with: - use-quiet-mode: 'yes' - config-file: '.github/markdown-link-checker-config.json' From 22a1fc9ec7000bf814621d04f7cca9c05c9ea84f Mon Sep 17 00:00:00 2001 From: Sebastian Opriel <22075788+SebastianOpriel@users.noreply.github.com> Date: Wed, 22 May 2024 17:01:08 +0200 Subject: [PATCH 226/295] Fixed typo in data-transfer-methods.md --- docs/getting-started/documentation/data-transfer-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/documentation/data-transfer-methods.md b/docs/getting-started/documentation/data-transfer-methods.md index 47cad3f25..77f69ffe2 100644 --- a/docs/getting-started/documentation/data-transfer-methods.md +++ b/docs/getting-started/documentation/data-transfer-methods.md @@ -1,7 +1,7 @@ Which data transfer methods are supported by the EDC-Connector? ======== -The connector supports three different data transfer modes: +The connector supports two different data transfer modes: 1. HTTPData: The provider EDC fetches the data from its own backend and pushes it to the consumer's desired data sink. 2. HTTPProxy: The provider EDC fetches the data and passes it on consumer's data transfer request synchronously back to the consumer. From 0508a38c2e0c597fabbce2003b8be6cc848e091d Mon Sep 17 00:00:00 2001 From: Sebastian Opriel <22075788+SebastianOpriel@users.noreply.github.com> Date: Wed, 22 May 2024 17:40:53 +0200 Subject: [PATCH 227/295] Update documentation for HttpData transfer (#929) * Update documentation for HttpData transfer * Update data-transfer-methods.md --------- Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- docs/getting-started/documentation/data-transfer-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/documentation/data-transfer-methods.md b/docs/getting-started/documentation/data-transfer-methods.md index 77f69ffe2..846cfadf2 100644 --- a/docs/getting-started/documentation/data-transfer-methods.md +++ b/docs/getting-started/documentation/data-transfer-methods.md @@ -3,7 +3,7 @@ Which data transfer methods are supported by the EDC-Connector? The connector supports two different data transfer modes: -1. HTTPData: The provider EDC fetches the data from its own backend and pushes it to the consumer's desired data sink. +1. HTTPData: The provider EDC fetches the data from the responsible data source and pushes the data to the consumer's desired data sink. The transfer flow is either initiated by the consumer using the EDC-UI or from a consumer backend via API calls, which could be a use case application and could also serve as a data sink (a1). This triggers an initiation for sending a transfer-request from the consumer control plane and the request is send towards the control plane of the provider. With this call (a2) the access credentials of the consumers data sink and its URL together with other necessary information are handed over to the provider. After successfully checking the context, e.g. that the requesting consumer is eligible to request the data, the provider control plane orchestrates a call to a provider data plane to process the transfer request further (a3). The provider data plane fetches the data from the responsible data source, as defined in the asset data-address and any additional parameterizations, with a REST call (a4) and caches the data (a5), to finally transfer the data to the desired consumer data sink (a6). 2. HTTPProxy: The provider EDC fetches the data and passes it on consumer's data transfer request synchronously back to the consumer. The following diagram illustrates the different transmission modes: From 4a523ba7febfb27386b9b0a5a5db53f3709a1013 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Tue, 28 May 2024 12:10:12 +0200 Subject: [PATCH 228/295] docs: expand postman-collection (#927) --- .github/ISSUE_TEMPLATE/release.md | 1 - .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/ci.yml | 5 - CHANGELOG.md | 2 + docs/api/postman_collection.json | 2826 ++++++++++++++++++++--------- 5 files changed, 1926 insertions(+), 909 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 0dc38085d..f9fc041b3 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -50,7 +50,6 @@ Feel free to edit this release checklist in-progress depending on what tasks nee the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] If the Eclipse EDC version changed, update the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-extensions/blob/main/docs/api/eclipse-edc-management-api.yaml). - - [ ] Update the Postman Collection if required. - [ ] Run all tests locally as long as the [GH flaky tests](https://github.com/sovity/edc-extensions/issues/870) are a problem. - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-extensions/actions). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c777e39aa..d324a40fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,5 +6,6 @@ _What issues does this PR close?_ - [ ] The PR title is short and expressive. - [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/authority-portal/tree/main/docs/dev/changelog_updates.md) for more information. - [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. +- [ ] I have updated the Community Edition [Postman-Collection](https://github.com/sovity/edc-extensions/blob/main/docs/api/postman_collection.json) if I changed existing APIs or added new APIs (e.g. for Management-API or API-Wrapper) - [ ] I have performed a **self-review** ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe921bdb0..31ab2b42f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,6 @@ on: types: [ published ] pull_request: branches: [ main ] - paths-ignore: - - '**.md' - - '**.png' - - 'docs/api/**' - - 'LICENSE' env: REGISTRY_URL: ghcr.io diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7690898..4afe72aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes +- Overhaul of the Postman-Collection + ### Deployment Migration Notes ## [7.5.0] - 2024-05-16 diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index b9dd8929c..181dfc094 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,904 +1,1924 @@ { - "info": { - "_postman_id": "c9b798b5-5495-49a2-830a-9f8718f34266", - "name": "sovity EDC (0.2.1)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Assets", - "item": [ - { - "name": "1 Create Asset", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"{{ASSET_ID}}\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\", \n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n }\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{PROVIDER_EDC_SOURCE_URL}}\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "1 Create Asset MDS", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"{{ASSET_ID}}\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n },\n \"https://w3id.org/mobilitydcat-ap/transport-mode\": \"Road\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category\": \"Traffic Information\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category\": \"Hazard Warnings\",\n \"https://w3id.org/mobilitydcat-ap/mobility-data-standard\": \"CSV\",\n \"https://w3id.org/mobilitydcat-ap/georeferencing-method\": \"Geo Ref Method Test\"\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{PROVIDER_EDC_SOURCE_URL}}\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "1 Delete Asset", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/assets/{{ASSET_ID}}", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "assets", - "{{ASSET_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "1 Request Assets", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/assets/request", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "assets", - "request" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Policies", - "item": [ - { - "name": "2 Create Simple Policy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/PolicyDefinition\",\n \"@id\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": []\n }\n ]\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "2 Create Time Policy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"@type\": \"PolicyDefinitionDto\",\n \"@id\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": [\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"POLICY_EVALUATION_TIME\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/gteq\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"2022-05-31T22:00:00.000Z\"\n },\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"POLICY_EVALUATION_TIME\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/lt\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"2030-06-30T22:00:00.000Z\"\n }\n ]\n }\n ]\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "2 Create Participant Policy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"@type\": \"PolicyDefinitionDto\",\n \"@id\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": [\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"REFERRING_CONNECTOR\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/eq\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"{{CONSUMER_EDC_PARTICIPANT_ID}}\"\n }\n ]\n }\n ]\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "2 Delete Policy", - "request": { - "method": "DELETE", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/{{POLICY_ID}}", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "policydefinitions", - "{{POLICY_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "2 Request Policies", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/request", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "policydefinitions", - "request" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "ContractDefinitions", - "item": [ - { - "name": "3 Create ContractDefinition", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"@id\": \"{{CONTRACT_DEFINITION_ID}}\",\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractDefinition\",\n \"https://w3id.org/edc/v0.0.1/ns/accessPolicyId\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/contractPolicyId\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/assetsSelector\": [\n {\n \"@type\": \"CriterionDto\",\n \"https://w3id.org/edc/v0.0.1/ns/operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"https://w3id.org/edc/v0.0.1/ns/operator\": \"=\",\n \"https://w3id.org/edc/v0.0.1/ns/operandRight\": \"{{ASSET_ID}}\"\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractdefinitions" - ] - } - }, - "response": [] - }, - { - "name": "3 Delete ContractDefinition", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/{{CONTRACT_DEFINITION_ID}}", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractdefinitions", - "{{CONTRACT_DEFINITION_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "3 Request ContractDefinitions", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/request", - "host": [ - "{{PROVIDER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractdefinitions", - "request" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Catalog", - "item": [ - { - "name": "4 Request Catalog", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/CatalogRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/counterPartyAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/querySpec\": {\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n }\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/catalog/request", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "catalog", - "request" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Contract Negotiations", - "item": [ - { - "name": "5 Start Negotiation", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/consumerId\": \"{{CONSUMER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/providerId\": \"{{PROVIDER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offer\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/offerId\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:ZjM4ZTJlMTItN2RmMC00ZjU3LTgwNDMtYjM0MzMwYTVkMDA3\",\r\n \"https://w3id.org/edc/v0.0.1/ns/assetId\": \"{{ASSET_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\r\n \"@id\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:ZjM4ZTJlMTItN2RmMC00ZjU3LTgwNDMtYjM0MzMwYTVkMDA3\",\r\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\r\n \"http://www.w3.org/ns/odrl/2/permission\": {\r\n \"http://www.w3.org/ns/odrl/2/target\": \"{{ASSET_ID}}\",\r\n \"http://www.w3.org/ns/odrl/2/action\": {\r\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\r\n },\r\n \"http://www.w3.org/ns/odrl/2/constraint\": []\r\n },\r\n \"http://www.w3.org/ns/odrl/2/prohibition\": [],\r\n \"http://www.w3.org/ns/odrl/2/obligation\": [],\r\n \"http://www.w3.org/ns/odrl/2/target\": \"{{ASSET_ID}}\"\r\n }\r\n }\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractnegotiations" - ] - } - }, - "response": [] - }, - { - "name": "5 Request Contract Negotiations", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/request", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractnegotiations", - "request" - ], - "query": [ - { - "key": "", - "value": "", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "5 Cancel Negotiation", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/3f009db0-775d-4dfc-a965-decdf5a76aea/cancel", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractnegotiations", - "3f009db0-775d-4dfc-a965-decdf5a76aea", - "cancel" - ] - } - }, - "response": [] - }, - { - "name": "5 Decline Negotiation", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/88687cb0-1d97-40c5-86c2-ad744afed538/decline", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractnegotiations", - "88687cb0-1d97-40c5-86c2-ad744afed538", - "decline" - ] - } - }, - "response": [] - }, - { - "name": "5 Get Negotiation", - "request": { - "method": "GET", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/88687cb0-1d97-40c5-86c2-ad744afed538", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractnegotiations", - "88687cb0-1d97-40c5-86c2-ad744afed538" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Contract Agreements", - "item": [ - { - "name": "6 Request Contract Agreements", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractagreements/request", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "contractagreements", - "request" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Data Transfer", - "item": [ - { - "name": "7 Start Data Push", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "ApiKeyDefaultValue", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TransferRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/assetId\": \"{{ASSET_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/contractId\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:MWZhMDk2YzEtODcwNi00NjBiLWJlMmYtZmQyNDFkZWQxYjE3\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorId\": \"{{PROVIDER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/dataDestination\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{CONSUMER_EDC_TRANSFER_TARGET_URL}}\"\r\n },\r\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {},\r\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/managedResources\": false\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "transferprocesses" - ] - } - }, - "response": [] - }, - { - "name": "8 Request Transfer Processes", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 10\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/request", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "transferprocesses", - "request" - ] - } - }, - "response": [] - }, - { - "name": "8 Cancel Transfer Process", - "request": { - "method": "POST", - "header": [ - { - "key": "X-Api-Key", - "value": "pass", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TerminateTransfer\",\r\n \"https://w3id.org/edc/v0.0.1/ns/reason\": \"Termination reason\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/c715355b-1e4b-49a9-9ef0-956405e88fe3/terminate", - "host": [ - "{{CONSUMER_EDC_MANAGEMENT_URL}}" - ], - "path": [ - "v2", - "transferprocesses", - "c715355b-1e4b-49a9-9ef0-956405e88fe3", - "terminate" - ] - } - }, - "response": [] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "key", - "value": "X-Api-Key", - "type": "string" - }, - { - "key": "value", - "value": "ApiKeyDefaultValue", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "variable": [ - { - "key": "PROVIDER_EDC_MANAGEMENT_URL", - "value": "http://localhost:11002/api/management", - "type": "default" - }, - { - "key": "PROVIDER_EDC_PROTOCOL_URL", - "value": "http://edc:11003/api/dsp", - "type": "default" - }, - { - "key": "PROVIDER_EDC_PARTICIPANT_ID", - "value": "my-edc", - "type": "default" - }, - { - "key": "PROVIDER_EDC_SOURCE_URL", - "value": "https://api.github.com/repos/sovity/edc-extensions/events", - "type": "default" - }, - { - "key": "CONSUMER_EDC_MANAGEMENT_URL", - "value": "http://localhost:22002/api/management", - "type": "default" - }, - { - "key": "CONSUMER_EDC_PROTOCOL_URL", - "value": "http://edc2:11003/api/dsp", - "type": "default" - }, - { - "key": "CONSUMER_EDC_PARTICIPANT_ID", - "value": "my-edc2", - "type": "default" - }, - { - "key": "CONSUMER_EDC_TRANSFER_TARGET_URL", - "value": "https://webhook.site/a418c986-299d-4e22-a1e1-bf532631913a", - "type": "default" - }, - { - "key": "COUNTER", - "value": "1", - "type": "default" - }, - { - "key": "ASSET_ID", - "value": "http-source-{{COUNTER}}", - "type": "default" - }, - { - "key": "POLICY_ID", - "value": "policy-{{COUNTER}}", - "type": "default" - }, - { - "key": "CONTRACT_DEFINITION_ID", - "value": "contract-definition-{{COUNTER}}", - "type": "default" - } - ] -} + "info": { + "_postman_id": "b6575425-f8be-4166-8c3e-cc7361909b9c", + "name": "sovity EDC Community Edition", + "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-extensions](https://github.com/sovity/edc-extensions)\n\nLicense: [https://github.com/sovity/edc-extensions/blob/main/LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "32949497" + }, + "item": [ + { + "name": "API-Wrapper", + "item": [ + { + "name": "UI Dashboard", + "item": [ + { + "name": "Get UI Dashboard Data", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/dashboard-page", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "dashboard-page" + ] + } + }, + "response": [] + }, + { + "name": "Get KPIs about the Connector", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/use-case-api/kpis", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "use-case-api", + "kpis" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Assets", + "item": [ + { + "name": "Get Assets", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page" + ] + } + }, + "response": [] + }, + { + "name": "Create Asset", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"testname-v1.0\",\r\n \"title\": \"TestName\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.google.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "Create Asset (with paramterization)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"testparamterization-v1.0\",\r\n \"title\": \"TestParamterization\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.endpoint.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/proxyMethod\": \"true\",\r\n \"https://w3id.org/edc/v0.0.1/ns/proxyPath\": \"true\",\r\n \"https://w3id.org/edc/v0.0.1/ns/proxyQueryParams\": \"true\",\r\n \"https://w3id.org/edc/v0.0.1/ns/proxyBody\": \"true\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "Create Asset (with auth-header)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"testparamterization-auth-header-v1.0\",\r\n \"title\": \"TestParamterization\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.endpoint.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/authKey\": \"key\",\r\n \"https://w3id.org/edc/v0.0.1/ns/secretName\": \"value\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "Edit Asset", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"testname-v1.0\",\r\n \"title\": \"TestName\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.google.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets/{{ASSET_ID}}/metadata", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page", + "assets", + "{{ASSET_ID}}", + "metadata" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Delete Assets", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets/{{ASSET_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page", + "assets", + "{{ASSET_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Policies", + "item": [ + { + "name": "Get Policies", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/policy-page", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "policy-page" + ] + } + }, + "response": [] + }, + { + "name": "Get Available Policy Functions", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/use-case-api/supported-policy-functions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "use-case-api", + "supported-policy-functions" + ] + } + }, + "response": [] + }, + { + "name": "Create Policy (Connector-Restricted-Usage)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"policyDefinitionId\": \"testPolicy\",\r\n \"policy\": {\r\n \"constraints\": [\r\n {\r\n \"left\": \"REFERRING_CONNECTOR\",\r\n \"operator\": \"EQ\",\r\n \"right\": {\r\n \"type\": \"STRING\",\r\n \"value\": \"other-connector-participant-id\"\r\n }\r\n }\r\n ]\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/policy-page/policy-definitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "policy-page", + "policy-definitions" + ] + } + }, + "response": [] + }, + { + "name": "Create Policy (Time-Period-Restricted)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"policyDefinitionId\": \"testTimeRestricted\",\r\n \"policy\": {\r\n \"constraints\": [\r\n {\r\n \"left\": \"POLICY_EVALUATION_TIME\",\r\n \"operator\": \"GEQ\",\r\n \"right\": {\r\n \"type\": \"STRING\",\r\n \"value\": \"2024-03-31T22:00:00.000Z\"\r\n }\r\n },\r\n {\r\n \"left\": \"POLICY_EVALUATION_TIME\",\r\n \"operator\": \"LT\",\r\n \"right\": {\r\n \"type\": \"STRING\",\r\n \"value\": \"2024-04-30T22:00:00.000Z\"\r\n }\r\n }\r\n ]\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/policy-page/policy-definitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "policy-page", + "policy-definitions" + ] + } + }, + "response": [] + }, + { + "name": "Delete Policy", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/policy-page/policy-definitions/{{POLICY_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "policy-page", + "policy-definitions", + "{{POLICY_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Definitions", + "item": [ + { + "name": "Get Contract Definitions", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-definition-page", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-definition-page" + ] + } + }, + "response": [] + }, + { + "name": "Create Contract Definition", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"contractDefinitionId\": \"testCD\",\r\n \"contractPolicyId\": \"always-true\",\r\n \"accessPolicyId\": \"always-true\",\r\n \"assetSelector\": [\r\n {\r\n \"operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\r\n \"operator\": \"IN\",\r\n \"operandRight\": {\r\n \"type\": \"VALUE_LIST\",\r\n \"valueList\": [\r\n \"testparamterization-v1.0\"\r\n ]\r\n }\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-definition-page/contract-definitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-definition-page", + "contract-definitions" + ] + } + }, + "response": [] + }, + { + "name": "Delete Contract Definition", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-definition-page/contract-definitions/{{CONTRACT_DEFINITION_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-definition-page", + "contract-definitions", + "{{CONTRACT_DEFINITION_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Catalog", + "item": [ + { + "name": "Request Catalog", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/catalog-page/data-offers?connectorEndpoint={{REQUESTING_DSP_ENDPOINT}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "catalog-page", + "data-offers" + ], + "query": [ + { + "key": "connectorEndpoint", + "value": "{{REQUESTING_DSP_ENDPOINT}}", + "description": "The full URL to the connector's DSP endpoint as seen by the connector. Example for the default docker compose setup (within the docker network): http://edc2:11003/api/dsp. You can also find this value in the dashboard's UI." + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Negotiations", + "item": [ + { + "name": "Start Negotiation", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"counterPartyAddress\": \"http://edc:11003/api/dsp\",\r\n \"counterPartyParticipantId\": \"my-edc\",\r\n \"contractOfferId\": \"dGVzdENE:dGVzdHBhcmFtdGVyaXphdGlvbi12MS4w:NTUxNjZiOGMtMzk4My00MWMzLTkxN2UtZTQ1ZGVlNzMyNTlj\",\r\n \"policyJsonLd\": \"{\\\"@id\\\":\\\"f15e4b97-0d99-42f5-8f6a-525daf0b72c6\\\",\\\"@type\\\":\\\"http://www.w3.org/ns/odrl/2/Set\\\",\\\"http://www.w3.org/ns/odrl/2/permission\\\":[{\\\"http://www.w3.org/ns/odrl/2/target\\\":\\\"testparamterization-v1.0\\\",\\\"http://www.w3.org/ns/odrl/2/action\\\":{\\\"http://www.w3.org/ns/odrl/2/type\\\":\\\"USE\\\"},\\\"http://www.w3.org/ns/odrl/2/constraint\\\":[{\\\"http://www.w3.org/ns/odrl/2/leftOperand\\\":{\\\"@value\\\":\\\"ALWAYS_TRUE\\\"},\\\"http://www.w3.org/ns/odrl/2/operator\\\":[{\\\"@id\\\":\\\"http://www.w3.org/ns/odrl/2/eq\\\"}],\\\"http://www.w3.org/ns/odrl/2/rightOperand\\\":{\\\"@value\\\":\\\"true\\\"}}]}],\\\"http://www.w3.org/ns/odrl/2/prohibition\\\":[],\\\"http://www.w3.org/ns/odrl/2/obligation\\\":[],\\\"http://www.w3.org/ns/odrl/2/target\\\":\\\"testparamterization-v1.0\\\"}\",\r\n \"assetId\": \"testparamterization-v1.0\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/catalog-page/contract-negotiations", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "catalog-page", + "contract-negotiations" + ] + } + }, + "response": [] + }, + { + "name": "Get Negotiation", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/catalog-page/contract-negotiations/{{NEGOTIATION_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "catalog-page", + "contract-negotiations", + "{{NEGOTIATION_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Agreement", + "item": [ + { + "name": "Get Contract Agreements", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-agreement-page", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-agreement-page" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Data Transfer", + "item": [ + { + "name": "Get Transfer History", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/transfer-history-page", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "transfer-history-page" + ] + } + }, + "response": [] + }, + { + "name": "Get Asset of Transfer Process", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/transfer-history-page/transfer-processes/{{TRANSFERPROCESS_ID}}/asset", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "transfer-history-page", + "transfer-processes", + "{{TRANSFERPROCESS_ID}}", + "asset" + ] + } + }, + "response": [] + }, + { + "name": "Initiate Transfer", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"contractAgreementId\": \"dGVzdENEd28=:dGVzdG5hbWUtdjEuMA==:OTI4ZmM4NzYtYzQ4MC00ODExLTgyMTEtMjhkYzRhZTk5MDEw\",\r\n \"dataSinkProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://webhook.site/b30aa8f1-2b47-42f5-b194-88d7c4ed80d4\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"POST\",\r\n \"https://w3id.org/edc/v0.0.1/ns/authKey\": \"authHeader\",\r\n \"https://w3id.org/edc/v0.0.1/ns/secretName\": \"test\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\",\r\n \"header:myHeader\": \"123\"\r\n },\r\n \"transferProcessProperties\": {}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-agreement-page/transfers", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-agreement-page", + "transfers" + ] + } + }, + "response": [] + }, + { + "name": "Initiate Transfer (with parameterization)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"contractAgreementId\": \"S0MtSlEtUGFycnJycmFt:a2MtanEtYXNzZXQtMg==:YmMzNDkzZWQtYWYzYy00YzE0LWExMzAtZTU0YzM3MzNlMjJk\",\r\n \"dataSinkProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://webhook.site/0c0b0148-cfac-4317-803a-ef17e8f5f9ec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"POST\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n },\r\n \"transferProcessProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"PUT\",\r\n \"https://w3id.org/edc/v0.0.1/ns/pathSegments\": \"icantread\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"fruit=tomato\",\r\n \"https://w3id.org/edc/v0.0.1/ns/body\": \"true\",\r\n \"https://w3id.org/edc/v0.0.1/ns/contentType\": \"application/json\",\r\n \"https://w3id.org/edc/v0.0.1/ns/mediaType\": \"application/json\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-agreement-page/transfers", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-agreement-page", + "transfers" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Management-API", + "item": [ + { + "name": "Assets", + "item": [ + { + "name": "Create Asset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"12345\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\", \n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n }\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.sovity.de\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "Create Asset (CE MDS)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"12345mds\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n },\n \"https://w3id.org/mobilitydcat-ap/transport-mode\": \"Road\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category\": \"Traffic Information\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category\": \"Hazard Warnings\",\n \"https://w3id.org/mobilitydcat-ap/mobility-data-standard\": \"CSV\",\n \"https://w3id.org/mobilitydcat-ap/georeferencing-method\": \"Geo Ref Method Test\"\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.sovity.de\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "Get Assets (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets/request", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets", + "request" + ] + } + }, + "response": [] + }, + { + "name": "Get Asset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets/{{ASSET_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets", + "{{ASSET_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Edit Asset Meta-Data (without DataAdress)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"12345\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document-edited\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\", \n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n }\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.sovity.de\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "assets" + ] + } + }, + "response": [] + }, + { + "name": "Edit Asset DataAddress", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"edc:DataAddress\",\n \"edc:type\": \"HttpData\",\n \"edc:proxyMethod\": \"false\",\n \"edc:proxyBody\": \"false\",\n \"edc:proxyPath\": \"false\",\n \"edc:baseUrl\": \"https://www.google.de\",\n \"@context\": {\n \"dct\": \"https://purl.org/dc/terms/\",\n \"tx\": \"https://w3id.org/tractusx/v0.0.1/ns/\",\n \"edc\": \"https://w3id.org/edc/v0.0.1/ns/\",\n \"dcat\": \"https://www.w3.org/ns/dcat/\",\n \"odrl\": \"http://www.w3.org/ns/odrl/2/\",\n \"dspace\": \"https://w3id.org/dspace/v0.8/\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/assets/{{ASSET_ID}}/dataaddress", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "assets", + "{{ASSET_ID}}", + "dataaddress" + ] + } + }, + "response": [] + }, + { + "name": "Delete Asset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v3/assets/{{ASSET_ID}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v3", + "assets", + "{{ASSET_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Policies", + "item": [ + { + "name": "Create Policy (Template)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/PolicyDefinition\",\n \"@id\": \"policy-1\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": []\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions" + ] + } + }, + "response": [] + }, + { + "name": "Create Policy (Connector-Restricted-Usage)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"PolicyDefinitionDto\",\n \"@id\": \"connector-restricted-policy\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": [\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"REFERRING_CONNECTOR\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/eq\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"{{CONSUMER_EDC_PARTICIPANT_ID}}\"\n }\n ]\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions" + ] + } + }, + "response": [] + }, + { + "name": "Create Policy (Time-Period-Restricted)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"PolicyDefinitionDto\",\n \"@id\": \"time-period-restricted-policy\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": [\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"POLICY_EVALUATION_TIME\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/gteq\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"2022-05-31T22:00:00.000Z\"\n },\n {\n \"http://www.w3.org/ns/odrl/2/leftOperand\": \"POLICY_EVALUATION_TIME\",\n \"http://www.w3.org/ns/odrl/2/operator\": {\n \"@id\": \"http://www.w3.org/ns/odrl/2/lt\"\n },\n \"http://www.w3.org/ns/odrl/2/rightOperand\": \"2030-06-30T22:00:00.000Z\"\n }\n ]\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions" + ] + } + }, + "response": [] + }, + { + "name": "Get Policies (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/request", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions", + "request" + ] + } + }, + "response": [] + }, + { + "name": "Get Policy", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/{{POLICY_NAME}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions", + "{{POLICY_NAME}}" + ] + } + }, + "response": [] + }, + { + "name": "Edit Policy (Template)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/PolicyDefinition\",\n \"@id\": \"policy-1\",\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\n \"http://www.w3.org/ns/odrl/2/permission\": [\n {\n \"http://www.w3.org/ns/odrl/2/action\": {\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\n },\n \"http://www.w3.org/ns/odrl/2/constraint\": []\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/{{POLICY_NAME}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions", + "{{POLICY_NAME}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Policy", + "request": { + "method": "DELETE", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/policydefinitions/{{POLICY_NAME}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "policydefinitions", + "{{POLICY_NAME}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Definitions", + "item": [ + { + "name": "Create Contract Definition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@id\": \"contractdefinition-1\",\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractDefinition\",\n \"https://w3id.org/edc/v0.0.1/ns/accessPolicyId\": \"always-true\",\n \"https://w3id.org/edc/v0.0.1/ns/contractPolicyId\": \"always-true\",\n \"https://w3id.org/edc/v0.0.1/ns/assetsSelector\": [\n {\n \"@type\": \"CriterionDto\",\n \"https://w3id.org/edc/v0.0.1/ns/operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"https://w3id.org/edc/v0.0.1/ns/operator\": \"=\",\n \"https://w3id.org/edc/v0.0.1/ns/operandRight\": \"12345\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions" + ] + } + }, + "response": [] + }, + { + "name": "Get Contract Definitions (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/request", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions", + "request" + ] + } + }, + "response": [] + }, + { + "name": "Get Contract Definition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/{{CONTRACT_DEFINITION_NAME}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions", + "{{CONTRACT_DEFINITION_NAME}}" + ] + } + }, + "response": [] + }, + { + "name": "Edit Contract Definition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"@id\": \"{{CONTRACT_DEFINITION_ID}}\",\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractDefinition\",\n \"https://w3id.org/edc/v0.0.1/ns/accessPolicyId\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/contractPolicyId\": \"{{POLICY_ID}}\",\n \"https://w3id.org/edc/v0.0.1/ns/assetsSelector\": [\n {\n \"@type\": \"CriterionDto\",\n \"https://w3id.org/edc/v0.0.1/ns/operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"https://w3id.org/edc/v0.0.1/ns/operator\": \"=\",\n \"https://w3id.org/edc/v0.0.1/ns/operandRight\": \"{{ASSET_ID}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions" + ] + } + }, + "response": [] + }, + { + "name": "Delete Contract Definition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/v2/contractdefinitions/{{CONTRACT_DEFINITION_NAME}}", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractdefinitions", + "{{CONTRACT_DEFINITION_NAME}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Catalog", + "item": [ + { + "name": "Request Catalog (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/CatalogRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/counterPartyAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/querySpec\": {\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/catalog/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "catalog", + "request" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Negotiations", + "item": [ + { + "name": "Start Negotiation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/ContractRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/consumerId\": \"{{CONSUMER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/providerId\": \"{{PROVIDER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offer\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/offerId\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:ZjM4ZTJlMTItN2RmMC00ZjU3LTgwNDMtYjM0MzMwYTVkMDA3\",\r\n \"https://w3id.org/edc/v0.0.1/ns/assetId\": \"{{ASSET_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/policy\": {\r\n \"@id\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:ZjM4ZTJlMTItN2RmMC00ZjU3LTgwNDMtYjM0MzMwYTVkMDA3\",\r\n \"@type\": \"http://www.w3.org/ns/odrl/2/Set\",\r\n \"http://www.w3.org/ns/odrl/2/permission\": {\r\n \"http://www.w3.org/ns/odrl/2/target\": \"{{ASSET_ID}}\",\r\n \"http://www.w3.org/ns/odrl/2/action\": {\r\n \"http://www.w3.org/ns/odrl/2/type\": \"USE\"\r\n },\r\n \"http://www.w3.org/ns/odrl/2/constraint\": []\r\n },\r\n \"http://www.w3.org/ns/odrl/2/prohibition\": [],\r\n \"http://www.w3.org/ns/odrl/2/obligation\": [],\r\n \"http://www.w3.org/ns/odrl/2/target\": \"{{ASSET_ID}}\"\r\n }\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations" + ] + } + }, + "response": [] + }, + { + "name": "Get Negotiation State", + "request": { + "method": "GET", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/{{NEGOTIATON_ID}}/state", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "{{NEGOTIATON_ID}}", + "state" + ] + } + }, + "response": [] + }, + { + "name": "Terminate Negotiation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TerminateNegotiation\",\r\n \"@id\": \"negotiation-id\",\r\n \"https://w3id.org/edc/v0.0.1/ns/reason\": \"a reason to terminate\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/88687cb0-1d97-40c5-86c2-ad744afed538/decline", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "88687cb0-1d97-40c5-86c2-ad744afed538", + "decline" + ] + } + }, + "response": [] + }, + { + "name": "Get Negotiations (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "request" + ], + "query": [ + { + "key": "", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Get Negotiation", + "request": { + "method": "GET", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/{{NEGOTIATON_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "{{NEGOTIATON_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Negotiation Agreement", + "request": { + "method": "GET", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractnegotiations/{{NEGOTIATON_ID}}/agreement", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractnegotiations", + "{{NEGOTIATON_ID}}", + "agreement" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Contract Agreement", + "item": [ + { + "name": "Get Contract Agreements (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractagreements/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractagreements", + "request" + ] + } + }, + "response": [] + }, + { + "name": "Get Contract Agreement", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractagreements/{{CONTRACT_AGREEMENT_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractagreements", + "{{CONTRACT_AGREEMENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Negotiation for Agreement", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/contractagreements/{{CONTRACT_AGREEMENT_ID}}/negotiation", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "contractagreements", + "{{CONTRACT_AGREEMENT_ID}}", + "negotiation" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Data Transfer", + "item": [ + { + "name": "Start Data Push", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "ApiKeyDefaultValue" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TransferRequest\",\r\n \"https://w3id.org/edc/v0.0.1/ns/assetId\": \"{{ASSET_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/contractId\": \"Y29udHJhY3QtZGVmaW5pdGlvbi0x:aHR0cC1zb3VyY2UtMQ==:MWZhMDk2YzEtODcwNi00NjBiLWJlMmYtZmQyNDFkZWQxYjE3\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorAddress\": \"{{PROVIDER_EDC_PROTOCOL_URL}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/connectorId\": \"{{PROVIDER_EDC_PARTICIPANT_ID}}\",\r\n \"https://w3id.org/edc/v0.0.1/ns/dataDestination\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"{{CONSUMER_EDC_TRANSFER_TARGET_URL}}\"\r\n },\r\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {},\r\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\r\n \"https://w3id.org/edc/v0.0.1/ns/protocol\": \"dataspace-protocol-http\",\r\n \"https://w3id.org/edc/v0.0.1/ns/managedResources\": false\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses" + ] + } + }, + "response": [] + }, + { + "name": "Get Transfer Processes (QuerySpec-Body)", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/QuerySpec\",\r\n \"https://w3id.org/edc/v0.0.1/ns/offset\": 0,\r\n \"https://w3id.org/edc/v0.0.1/ns/limit\": 1000\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/request", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses", + "request" + ] + } + }, + "response": [] + }, + { + "name": "Terminate Transfer Process", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Api-Key", + "value": "pass" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/TerminateTransfer\",\r\n \"https://w3id.org/edc/v0.0.1/ns/reason\": \"Termination reason\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/c715355b-1e4b-49a9-9ef0-956405e88fe3/terminate", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses", + "c715355b-1e4b-49a9-9ef0-956405e88fe3", + "terminate" + ] + } + }, + "response": [] + }, + { + "name": "Get Transfer", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/{{TRANSFER_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses", + "{{TRANSFER_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Transfer State", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/v2/transferprocesses/{{TRANSFER_ID}}/state", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "v2", + "transferprocesses", + "{{TRANSFER_ID}}", + "state" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "key", + "value": "X-Api-Key", + "type": "string" + }, + { + "key": "value", + "value": "ApiKeyDefaultValue", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "PROVIDER_EDC_MANAGEMENT_URL", + "value": "http://localhost:11002/api/management", + "type": "default" + }, + { + "key": "PROVIDER_EDC_PROTOCOL_URL", + "value": "http://edc:11003/api/dsp", + "type": "default" + }, + { + "key": "PROVIDER_EDC_PARTICIPANT_ID", + "value": "my-edc", + "type": "default" + }, + { + "key": "PROVIDER_EDC_SOURCE_URL", + "value": "https://api.github.com/repos/sovity/edc-extensions/events", + "type": "default" + }, + { + "key": "CONSUMER_EDC_MANAGEMENT_URL", + "value": "http://localhost:22002/api/management", + "type": "default" + }, + { + "key": "CONSUMER_EDC_PROTOCOL_URL", + "value": "http://edc2:11003/api/dsp", + "type": "default" + }, + { + "key": "CONSUMER_EDC_PARTICIPANT_ID", + "value": "my-edc2", + "type": "default" + }, + { + "key": "CONSUMER_EDC_TRANSFER_TARGET_URL", + "value": "https://webhook.site/a418c986-299d-4e22-a1e1-bf532631913a", + "type": "default" + }, + { + "key": "COUNTER", + "value": "1", + "type": "default" + }, + { + "key": "ASSET_ID", + "value": "http-source-{{COUNTER}}", + "type": "default" + }, + { + "key": "POLICY_ID", + "value": "policy-{{COUNTER}}", + "type": "default" + }, + { + "key": "CONTRACT_DEFINITION_ID", + "value": "contract-definition-{{COUNTER}}", + "type": "default" + } + ] +} \ No newline at end of file From a05a1c504fb5a64379868124dea6aacf4514266d Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Mon, 27 May 2024 15:33:10 +0200 Subject: [PATCH 229/295] Integrate the broker's extensions (#921) * Add accidentally deleted SUMMARY * chore: Apply new config style * Code review * Merge docs * Merge trivial templates changes * Merge more conflicting files * Merge more conflicting files * Merge more conflicting files and cleanup old versions * Add connector submodule * Merge more conflicting files * Move broker CHANGELOG * Move the broker's README * More cleaning * Fix links * Prepare docker build to merge the broker * Merge the docker build * Fix links and format * Add connector type info * self review * Convert MD table to html * Only use test containers for the Broker * Add broker NPM delivery * code review * Add broker local dev doc * Merge deployment READMEs and change healthcheck header * Fix links * Cleanup * Fix javadocs and add -sources and -javadocs artefacts * Rename .env.extensions to .env.connector * Rename IDS broker -> broker * Remove redundant .editorconfig setting * Change connector-type -> deployment-type in workflow config * NPM clean install * Removed mention of the broker in the main readme * Dry postgres image name * Remove test DB deprecation * Move broker group to toml * Fix missing broker's groups * More versions cleanup * Change MD for HTML * Merge the broker compose file * https://github.com/sovity/edc-extensions/pull/921#discussion_r1604695043 * Merge local quick start for broker * Align postgres versions * Stop annoying me with DB not ready messages on new startup * Wait for the DBs * Code review * remove docs task. The repo is now public * Avoid star imports * Remove reference to the merged doc --- .dockerignore | 6 +- .editorconfig | 20 + .env | 1 + .env.dev | 2 +- .github/ISSUE_TEMPLATE/feature_request_mds.md | 8 + .../ISSUE_TEMPLATE/mds_enhancement.md | 0 .../actions/build-connector-image/action.yml | 6 +- .github/workflows/ci.yml | 82 +++ .github/workflows/code_analysis.yml | 6 +- .github/workflows/license_scan.yml | 6 +- .github/workflows/secret_scan.yml | 2 +- .gitignore | 5 + CHANGELOG.md | 10 +- README.md | 2 +- SECURITY.md | 3 +- SUMMARY.md | 48 ++ {conflicts => archived/broker}/README.md | 32 +- build.gradle.kts | 5 + conflicts/.dockerignore | 3 - conflicts/.editorconfig | 21 - conflicts/.env | 4 - conflicts/.gitattributes | 102 --- .../.github/ISSUE_TEMPLATE/bug_report.yaml | 62 -- conflicts/.github/ISSUE_TEMPLATE/config.yml | 1 - .../.github/ISSUE_TEMPLATE/documentation.md | 30 - .../.github/ISSUE_TEMPLATE/epic_template.md | 41 -- .../.github/ISSUE_TEMPLATE/feature_request.md | 34 - .../ISSUE_TEMPLATE/mds_feature_request.md | 47 -- conflicts/.github/ISSUE_TEMPLATE/process.md | 24 - conflicts/.github/ISSUE_TEMPLATE/release.md | 4 +- conflicts/.github/PULL_REQUEST_TEMPLATE.md | 10 - .../workflows/add_issue_to_project.yml | 16 - .../build-and-publish-connector-images.yml | 97 --- .../build-and-publish-ts-api-client.yml | 65 -- conflicts/.github/workflows/code_analysis.yml | 59 -- conflicts/.github/workflows/codeql.yml | 49 -- conflicts/.github/workflows/license_scan.yml | 39 -- .../.github/workflows/release_docs_zip.yml | 27 - conflicts/.github/workflows/secret_scan.yml | 25 - conflicts/.github/workflows/security_scan.yml | 29 - conflicts/.pre-commit-README.md | 25 - conflicts/.pre-commit-config.yaml | 7 - conflicts/CHANGELOG.md | 645 ------------------ conflicts/CODE_OF_CONDUCT.md | 70 -- conflicts/CONTRIBUTING.md | 162 ----- conflicts/LICENSE.md | 201 ------ conflicts/SECURITY.md | 32 - conflicts/STYLEGUIDE.md | 58 -- conflicts/build.gradle.kts | 86 --- conflicts/docker-compose.yaml | 113 --- conflicts/docs/dev/changelog_updates.md | 60 -- .../docs/dev/checkstyle/checkstyle-config.xml | 416 ----------- conflicts/gradle.properties | 26 - .../gradle/wrapper/gradle-wrapper.properties | 5 - conflicts/gradlew | 234 ------- conflicts/gradlew.bat | 89 --- conflicts/settings.gradle.kts | 7 - connector/Dockerfile | 54 -- connector/build.gradle.kts | 43 -- .../src/main/resources/logging.properties | 8 - docker-compose-dev.yaml | 79 ++- docker-compose.yaml | 90 ++- .../goals/broker-production/README.md | 130 ++++ .../goals/local-demo/README.md | 20 +- .../goals/production/4.2.0/README.md | 8 +- .../goals/production/README.md | 5 +- docs/dev/changelog_updates.md | 7 +- .../broker-server-api/api/build.gradle.kts | 40 +- .../broker-server-api/client/build.gradle.kts | 74 +- .../build.gradle.kts | 38 +- extensions/broker-server/README.md | 2 +- extensions/broker-server/build.gradle.kts | 84 ++- .../brokerserver/db/TestDatabaseFactory.java | 6 +- .../db/TestDatabaseViaTestcontainers.java | 3 +- extensions/edc-ui-config/build.gradle.kts | 9 +- extensions/last-commit-info/build.gradle.kts | 10 +- .../policy-always-true/build.gradle.kts | 6 +- .../build.gradle.kts | 7 +- .../policy-time-interval/build.gradle.kts | 5 +- extensions/postgres-flyway/README.md | 2 +- extensions/postgres-flyway/build.gradle.kts | 10 +- .../build.gradle.kts | 6 +- .../test-backend-controller/build.gradle.kts | 6 +- .../build.gradle.kts | 6 +- .../clients/java-client/build.gradle.kts | 16 +- .../wrapper/wrapper-api/build.gradle.kts | 15 +- .../api/ui/model/UiCriterionOperator.java | 2 +- .../wrapper-common-api/build.gradle.kts | 6 +- .../wrapper-common-mappers/build.gradle.kts | 10 +- .../wrapper/wrapper-ee-api/build.gradle.kts | 6 +- extensions/wrapper/wrapper/build.gradle.kts | 12 +- .../ContractNegotiationStateService.java | 2 +- gradle.properties | 22 - gradle/libs.versions.toml | 59 +- connector/.env => launchers/.env.broker | 2 +- launchers/{.env => .env.connector} | 2 +- launchers/Dockerfile | 8 +- launchers/README.md | 141 +++- launchers/common/auth-daps/build.gradle.kts | 6 +- launchers/common/auth-mock/build.gradle.kts | 6 +- launchers/common/base-mds/build.gradle.kts | 3 +- launchers/common/base/build.gradle.kts | 6 +- .../common/observability/build.gradle.kts | 6 +- .../broker-server-ce/build.gradle.kts | 35 + .../broker-server-dev/build.gradle.kts | 35 + launchers/connectors/mds-ce/build.gradle.kts | 6 +- .../connectors/sovity-ce/build.gradle.kts | 6 +- .../connectors/sovity-dev/build.gradle.kts | 3 +- .../connectors/test-backend/build.gradle.kts | 7 +- launchers/logging.properties | 1 + settings.gradle.kts | 8 + tests/build.gradle.kts | 12 +- utils/catalog-parser/build.gradle.kts | 10 +- utils/json-and-jsonld-utils/build.gradle.kts | 10 +- utils/test-connector-remote/build.gradle.kts | 11 +- .../e2e/db/TestDatabaseViaTestcontainers.java | 3 +- utils/test-utils/build.gradle.kts | 12 +- utils/versions/build.gradle.kts | 57 ++ 118 files changed, 1005 insertions(+), 3568 deletions(-) rename conflicts/.github/ISSUE_TEMPLATE/enhancement.md => .github/ISSUE_TEMPLATE/mds_enhancement.md (100%) create mode 100644 SUMMARY.md rename {conflicts => archived/broker}/README.md (84%) delete mode 100644 conflicts/.dockerignore delete mode 100644 conflicts/.editorconfig delete mode 100644 conflicts/.env delete mode 100644 conflicts/.gitattributes delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/bug_report.yaml delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/config.yml delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/documentation.md delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/epic_template.md delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/mds_feature_request.md delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/process.md delete mode 100644 conflicts/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 conflicts/.github/workflows/add_issue_to_project.yml delete mode 100644 conflicts/.github/workflows/build-and-publish-connector-images.yml delete mode 100644 conflicts/.github/workflows/build-and-publish-ts-api-client.yml delete mode 100644 conflicts/.github/workflows/code_analysis.yml delete mode 100644 conflicts/.github/workflows/codeql.yml delete mode 100644 conflicts/.github/workflows/license_scan.yml delete mode 100644 conflicts/.github/workflows/release_docs_zip.yml delete mode 100644 conflicts/.github/workflows/secret_scan.yml delete mode 100644 conflicts/.github/workflows/security_scan.yml delete mode 100644 conflicts/.pre-commit-README.md delete mode 100644 conflicts/.pre-commit-config.yaml delete mode 100644 conflicts/CHANGELOG.md delete mode 100644 conflicts/CODE_OF_CONDUCT.md delete mode 100644 conflicts/CONTRIBUTING.md delete mode 100644 conflicts/LICENSE.md delete mode 100644 conflicts/SECURITY.md delete mode 100644 conflicts/STYLEGUIDE.md delete mode 100644 conflicts/build.gradle.kts delete mode 100644 conflicts/docker-compose.yaml delete mode 100644 conflicts/docs/dev/changelog_updates.md delete mode 100644 conflicts/docs/dev/checkstyle/checkstyle-config.xml delete mode 100644 conflicts/gradle.properties delete mode 100644 conflicts/gradle/wrapper/gradle-wrapper.properties delete mode 100755 conflicts/gradlew delete mode 100644 conflicts/gradlew.bat delete mode 100644 conflicts/settings.gradle.kts delete mode 100644 connector/Dockerfile delete mode 100644 connector/build.gradle.kts delete mode 100644 connector/src/main/resources/logging.properties create mode 100644 docs/deployment-guide/goals/broker-production/README.md rename connector/.env => launchers/.env.broker (98%) rename launchers/{.env => .env.connector} (98%) create mode 100644 launchers/connectors/broker-server-ce/build.gradle.kts create mode 100644 launchers/connectors/broker-server-dev/build.gradle.kts create mode 100644 utils/versions/build.gradle.kts diff --git a/.dockerignore b/.dockerignore index 0525fc2de..9c895ca99 100644 --- a/.dockerignore +++ b/.dockerignore @@ -38,7 +38,8 @@ build *.hprof .env -!launchers/.env +!launchers/.env.connector +!launchers/.env.broker # Log files *.log @@ -48,3 +49,6 @@ build **/*.jks docs/secrets + +node_modules +npm-debug.log diff --git a/.editorconfig b/.editorconfig index 2439c0ec6..4476e7487 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,23 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +indent_size = 4 + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + +[{*.kt,*.kts}] +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_packages_to_use_import_on_demand = explicitly-none +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_continuation_indent_size = 4 + diff --git a/.env b/.env index a0d3c036f..6b42e53c1 100644 --- a/.env +++ b/.env @@ -3,3 +3,4 @@ EDC_IMAGE=ghcr.io/sovity/edc-dev:7.4.2 TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:7.4.2 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 EDC_UI_ACTIVE_PROFILE=sovity-open-source +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.2.0 diff --git a/.env.dev b/.env.dev index 8c017456b..c6574ad4f 100644 --- a/.env.dev +++ b/.env.dev @@ -3,4 +3,4 @@ EDC_IMAGE=ghcr.io/sovity/edc-dev:latest TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:latest EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest EDC_UI_ACTIVE_PROFILE=sovity-open-source - +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest diff --git a/.github/ISSUE_TEMPLATE/feature_request_mds.md b/.github/ISSUE_TEMPLATE/feature_request_mds.md index 510c0cb95..0929e93b7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request_mds.md +++ b/.github/ISSUE_TEMPLATE/feature_request_mds.md @@ -26,6 +26,14 @@ _What problems does that user face that existing functionalities do solve?_ _Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new innovative ideas?_ +## MDS Scope check + +_Is this feature part of the contracted scope?_ +- [ ] It is part of the contracted scope +- [ ] It is not part of the contracted scope + +If not, please add the label "mds/future-scope" + ## (For sovity Team to complete) Stakeholders _Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ diff --git a/conflicts/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/mds_enhancement.md similarity index 100% rename from conflicts/.github/ISSUE_TEMPLATE/enhancement.md rename to .github/ISSUE_TEMPLATE/mds_enhancement.md diff --git a/.github/actions/build-connector-image/action.yml b/.github/actions/build-connector-image/action.yml index 1dfbefa10..e9d991b56 100644 --- a/.github/actions/build-connector-image/action.yml +++ b/.github/actions/build-connector-image/action.yml @@ -19,6 +19,9 @@ inputs: connector-name: required: true description: "EDC Connector Name in launchers/connectors/{connector-name}" + deployment-type: + required: true + description: "Type of deployment: 'connector' or 'broker'" title: required: true description: "Docker Image Title" @@ -62,7 +65,7 @@ runs: type=raw,value=latest,enable={{is_default_branch}} type=raw,value=release,enable=${{ startsWith(github.ref, 'refs/tags/') }} - name: "Docker: Build and Push" - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: file: launchers/Dockerfile context: . @@ -71,5 +74,6 @@ runs: labels: ${{ steps.meta.outputs.labels }} build-args: | CONNECTOR_NAME=${{ inputs.connector-name }} + CONNECTOR_TYPE=${{ inputs.deployment-type }} "EDC_LAST_COMMIT_INFO_ARG=${{ env.LAST_COMMIT_INFO }}" EDC_BUILD_DATE_ARG=${{ env.BUILD_DATE }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68abaac9d..1b5121052 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,7 @@ jobs: image-base-name: ${{ env.IMAGE_BASE_NAME }} image-name: "edc-dev" connector-name: "sovity-dev" + deployment-type: "connector" title: "sovity Dev EDC Connector" description: "Extended EDC Connector built by sovity. This dev version contains no dataspace auth and can be used to quickly start a locally running EDC + EDC UI." - name: "Docker Image: edc-ce" @@ -76,6 +77,7 @@ jobs: image-base-name: ${{ env.IMAGE_BASE_NAME }} image-name: "edc-ce" connector-name: "sovity-ce" + deployment-type: "connector" title: "sovity Community Edition EDC Connector" description: "EDC Connector built by sovity. Contains sovity's Community Edition EDC extensions and requires dataspace credentials to join an existing dataspace." - name: "Docker Image: edc-ce-mds" @@ -87,6 +89,7 @@ jobs: image-base-name: ${{ env.IMAGE_BASE_NAME }} image-name: "edc-ce-mds" connector-name: "mds-ce" + deployment-type: "connector" title: "MDS Community Edition EDC Connector" description: "EDC Connector built by sovity and configured for compatibility with the Mobility Data Space (MDS). This EDC requires dataspace credentials, and additional MDS Services such as a Clearing House." - name: "Docker Image: test-backend" @@ -98,8 +101,33 @@ jobs: image-base-name: ${{ env.IMAGE_BASE_NAME }} image-name: "test-backend" connector-name: "test-backend" + deployment-type: "connector" title: "Test Data Source / Data Sink" description: "Provides a minimal data source / data sink for E2E tests." + - name: "Docker Image: broker-server-dev" + uses: ./.github/actions/build-connector-image + with: + registry-url: ${{ env.REGISTRY_URL }} + registry-user: ${{ env.REGISTRY_USER }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + image-base-name: ${{ env.IMAGE_BASE_NAME }} + image-name: "broker-server-dev" + connector-name: "broker-server-dev" + deployment-type: "broker" + title: "Broker Server (Dev)" + description: "sovity EDC Broker Server. This dev version contains no auth and can be used to quickly start a locally running Broker Server + Broker UI." + - name: "Docker Image: broker-server-ce" + uses: ./.github/actions/build-connector-image + with: + registry-url: ${{ env.REGISTRY_URL }} + registry-user: ${{ env.REGISTRY_USER }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + image-base-name: ${{ env.IMAGE_BASE_NAME }} + image-name: "broker-server-ce" + connector-name: "broker-server-ce" + deployment-type: "broker" + title: "Broker Server (Community Edition)" + description: "sovity EDC Broker Server. Requires dataspace credentials to join an existing dataspace." ts-api-client-library: name: TS API Client Library runs-on: ubuntu-latest @@ -153,6 +181,60 @@ jobs: env: NODE_USER: richardtreier-sovity NODE_AUTH_TOKEN: ${{ secrets.SOVITY_EDC_CLIENT_NPM_AUTH }} + ts-broker-api-client-library: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + + steps: + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + - uses: actions/setup-node@v4 + with: + node-version: 16 + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + cache-dependency-path: ./extensions/broker-server-api/client-ts/package-lock.json + - name: Generate openapi.yaml & Client Code + run: | + ./gradlew :extensions:broker-server-api:api:clean :extensions:broker-server-api:api:build -x test --no-daemon + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: NPM Package Dist Tag & Version + working-directory: ./extensions/broker-server-api/client-ts + run: | + if [ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]; then + # Full Release + VERSION="${GITHUB_REF#refs/tags/v}" + DIST_TAG=latest + else + VERSION="0.$(date '+%Y%m%d.%H%M%S')-main-$CI_SHA_SHORT" + DIST_TAG=main + fi + npm version $VERSION + echo "DIST_TAG=$DIST_TAG" >> $GITHUB_ENV + - name: Build NPM Library + working-directory: ./extensions/broker-server-api/client-ts + run: | + npm ci && npm run build + - name: Publish NPM Library + if: github.ref == 'refs/heads/main' || github.ref == 'refs/tags/v*' + working-directory: ./extensions/broker-server-api/client-ts + run: | + npm set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN + npm set //registry.npmjs.org/:username $NODE_USER + npm publish --access public --tag "${{ env.DIST_TAG }}" + env: + NODE_USER: richardtreier-sovity + NODE_AUTH_TOKEN: ${{ secrets.SOVITY_BROKER_SERVER_CLIENT_NPM_AUTH }} markdown-link-checks: name: Markdown Link Checks runs-on: ubuntu-latest diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml index 0e9cab29b..74afd292f 100644 --- a/.github/workflows/code_analysis.yml +++ b/.github/workflows/code_analysis.yml @@ -17,7 +17,7 @@ jobs: spotbugs_active: ${{ steps.check_spotbugs.outputs.spotbugs_active }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check file existence id: check_files uses: andstor/file-existence-action@v2 @@ -34,7 +34,7 @@ jobs: if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.checkstyle_active == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v3 with: @@ -48,7 +48,7 @@ jobs: if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.spotbugs_active == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v3 with: diff --git a/.github/workflows/license_scan.yml b/.github/workflows/license_scan.yml index ebd597ac6..84d865164 100644 --- a/.github/workflows/license_scan.yml +++ b/.github/workflows/license_scan.yml @@ -28,9 +28,11 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: npm install (typescript-client) - run: cd extensions/wrapper/clients/typescript-client && npm install + run: cd extensions/wrapper/clients/typescript-client && npm clean-install - name: npm install (typescript-client-example) - run: cd extensions/wrapper/clients/typescript-client-example && npm install + run: cd extensions/wrapper/clients/typescript-client-example && npm clean-install + - name: npm install (client-ts) + run: cd extensions/broker-server-api/client-ts && npm clean-install - name: Run license scanner uses: aquasecurity/trivy-action@master with: diff --git a/.github/workflows/secret_scan.yml b/.github/workflows/secret_scan.yml index b27e1f6b1..613fc5682 100644 --- a/.github/workflows/secret_scan.yml +++ b/.github/workflows/secret_scan.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run vulnerability scanner uses: aquasecurity/trivy-action@master with: diff --git a/.gitignore b/.gitignore index 015627351..2d9fa213e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,15 @@ build **/.env !.env <<<<<<< HEAD +<<<<<<< HEAD !launchers/.env ======= !connector/.env >>>>>>> refs/rewritten/Merge-unrelated-tree-from-broker-into-edc-extensions +======= +!launchers/.env.connector +!launchers/.env.broker +>>>>>>> f6fe5d0e (Integrate the broker's extensions (#921)) # Log files *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index a157402dd..f6567291b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,17 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Overview +Starting from version `8`, the Broker has been merged with the Community edition. + +[The former changelog](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for the Broker is still available but will not be updated anymore. + +The Broker's version therefore jumps from version 4 to version 8. + +The functionalities of each part, Broker and Extensions, on this release, is the same as before the change. + ### EDC UI -### EDC Extensions +### EDC Extensions and Broker #### Major Changes diff --git a/README.md b/README.md index ba070d181..2d2b8ca1d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Check out our [Getting Started Section](#getting-started) on how to run a local Our sovity Community Edition EDC takes available Open Source EDC Extensions and combines them with our own open source EDC Extensions from this repository to build ready-to-use EDC Docker Images. -See [here](launchers/README.md) for a list of our sovity Community Edition EDC Docker Images. +See [here](launchers/README.md) for a list of our sovity Community Edition EDC Docker images.

      (back to top)

      diff --git a/SECURITY.md b/SECURITY.md index 1b1c0bcd5..9b18b227a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,4 +29,5 @@ This information will help us triage your report more quickly. We prefer all communications to be in English. ## Attribution -This file is adapted from [eclipse-edc](https://github.com/eclipse-edc/DataDashboard) project. \ No newline at end of file + +This file is adapted from [eclipse-edc/DataDashboard](https://github.com/eclipse-edc/DataDashboard) project. diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 000000000..ea8baafc3 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,48 @@ +# Summary + +* [Start](./README.md) +* [Connector Versions](./launchers/README.md) +* [Changelog](./CHANGELOG.md) + + +## User Documentation + +* [Getting Started](./docs/getting-started/README.md) +* [Data Transfer Modes](./docs/getting-started/documentation/data-transfer-methods.md) +* [API Wrapper](./docs/getting-started/documentation/api_wrapper.md) +* [OAuth Data Address](./docs/getting-started/documentation/oauth-data-address.md) +* [Parameterized Assets via UI](./docs/getting-started/documentation/parameterized_assets_via_ui.md) +* [Parameterized Assets via Managment API](./docs/getting-started/documentation/parameterized_assets.md) +* [Pull Data Transfer](./docs/getting-started/documentation/pull-data-transfer.md) + +## Deployment Documentation +* [Deployment Goal: Local Demo](./docs/deployment-guide/goals/local-demo) +* [Deployment Goal: Development](./docs/deployment-guide/goals/development) +* [Deployment Goal: Production](./docs/deployment-guide/goals/production) + * [Productive Deployment Guide](./docs/deployment-guide/goals/production) + * [Productive Deployment Guide 4.2.0 / MS8 / MDS 1.2](docs/deployment-guide/goals/production/4.2.0/README.md) + +## Developer Documentation + +* [Code of Conduct](./CODE_OF_CONDUCT.md) +* [Contribution Guide](./CONTRIBUTING.md) +* [Code-Style Guide](./STYLEGUIDE.md) +* [Security Guide](./SECURITY.md) + + +## Extensions + +* [API Wrapper](./extensions/wrapper/README.md) + * [Community Edition API](./extensions/wrapper/wrapper-api/README.md) + * [Enterprise Edition API](./extensions/wrapper/wrapper-ee-api/README.md) + * [Java API Client Library](./extensions/wrapper/clients/java-client/README.md) + * [Java API Client Library Example](./extensions/wrapper/clients/java-client-example/README.md) + * [TypeScript API Client Library](./extensions/wrapper/clients/typescript-client/README.md) + * [TypeScript API Client Library Example](./extensions/wrapper/clients/typescript-client-example/README.md) +* Policies + * [Always True](./extensions/policy-always-true/README.md) + * [Referring Connector](./extensions/policy-referring-connector/README.md) + * [Time Interval](./extensions/policy-time-interval/README.md) +* [Database Migration](./extensions/postgres-flyway/README.md) +* [EDC UI Config](./extensions/edc-ui-config/README.md) +* [Last Commit Info](./extensions/last-commit-info/README.md) diff --git a/conflicts/README.md b/archived/broker/README.md similarity index 84% rename from conflicts/README.md rename to archived/broker/README.md index 737bff12a..99582f89b 100644 --- a/conflicts/README.md +++ b/archived/broker/README.md @@ -99,7 +99,7 @@ the [docker-compose.yaml](#local-demo). ### Local Demo -There is a [docker-compose.yaml](docker-compose.yaml) that starts a broker and a connector. +There is a [docker-compose.yaml](../../docker-compose.yaml) that starts a broker and a connector. At release time it is pinned down to the release versions. @@ -124,30 +124,28 @@ Mid-development it might be un-pinned back to latest versions. ### Deployment Units -| Deployment Unit | Version / Details | -|----------------------------------------------------------------|-----------------------------------------------------------------------------| -| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | -| Postgresql | 15 or compatible version | -| Broker Backend | broker-server-ce, see [CHANGELOG.md](CHANGELOG.md) for compatible versions. | -| Broker UI | edc-ui, see [CHANGELOG.md](CHANGELOG.md) for compatible versions. | +| Deployment Unit | Version / Details | +|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | +| Postgresql | 15 or compatible version | +| Broker Backend | broker-server-ce, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for compatible versions. | +| Broker UI | edc-ui, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for compatible versions. | ### Configuration -There is a [docker-compose.yaml](docker-compose.yaml) to try out the broker locally. However, a productive release will -require a few more configuration options, so you should only use it to check if the released version is roughly working -or if it's broken. +There is a [docker-compose.yaml](../../docker-compose.yaml) to try out the broker locally. However, a productive release will require a few more configuration options, so you should only use it to check if the released version is roughly working or if it's broken. #### Reverse Proxy Configuration - The broker is meant to be served via TLS/HTTPS. - The broker is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `8080` port. - - The Backend's `11002` port. - - The Backend's `11003` port. + - The UI's `8080` port. + - The Backend's `11002` port. + - The Backend's `11003` port. - The mapping should look like this: - - `https://[MY_EDC_FQDN]/backend/api/dsp` -> `broker-backend:11003/backend/api/dsp` - - `https://[MY_EDC_FQDN]/backend/api/management` -> `broker-backend:11002/backend/api/management` - - All other requests -> `broker-ui:8080` + - `https://[MY_EDC_FQDN]/backend/api/dsp` -> `broker-backend:11003/backend/api/dsp` + - `https://[MY_EDC_FQDN]/backend/api/management` -> `broker-backend:11002/backend/api/management` + - All other requests -> `broker-ui:8080` #### Backend Configuration @@ -192,7 +190,7 @@ EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey ``` All pre-configured config values for either the broker server or the underlying EDC can be found -in [connector/.env](.). TODO: fix url after merge +in [launchers/.env.broker](../../launchers/.env.broker). #### UI Configuration diff --git a/build.gradle.kts b/build.gradle.kts index 06e204e1f..aed53cee1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -121,4 +121,9 @@ subprojects { } } } + + java { + withSourcesJar() + withJavadocJar() + } } diff --git a/conflicts/.dockerignore b/conflicts/.dockerignore deleted file mode 100644 index c81b8d319..000000000 --- a/conflicts/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -npm-debug.log -.env diff --git a/conflicts/.editorconfig b/conflicts/.editorconfig deleted file mode 100644 index 0f78b5fda..000000000 --- a/conflicts/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# Editor configuration, see https://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 4 -insert_final_newline = true -trim_trailing_whitespace = true -end_of_line = lf - -[*.ts] -quote_type = single - -[*.md] -max_line_length = off -trim_trailing_whitespace = false - -[docker-compose.yaml] -indent_size = 2 - diff --git a/conflicts/.env b/conflicts/.env deleted file mode 100644 index bbe3d5a3c..000000000 --- a/conflicts/.env +++ /dev/null @@ -1,4 +0,0 @@ -# Config for docker-compose.yaml -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.2.0 -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.4.2 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 diff --git a/conflicts/.gitattributes b/conflicts/.gitattributes deleted file mode 100644 index 8510e87df..000000000 --- a/conflicts/.gitattributes +++ /dev/null @@ -1,102 +0,0 @@ -* text=auto eol=lf - -# Web -*.css text diff=css -*.scss text diff=css -*.htm text diff=html -*.html text diff=html -*.properties text eol=lf - -# Exclude files from exporting -.gitattributes export-ignore -.gitignore export-ignore -.github export-ignore - -# Scripts -*.bash text eol=lf -*.fish text eol=lf -*.sh text eol=lf - -# Windows Scripts need crlf -*.bat text eol=crlf -*.cmd text eol=crlf -*.ps1 text eol=crlf - -# Documents -*.tex text diff=tex -*.bibtex text diff=bibtex -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain - -# Graphics -*.ai binary -*.bmp binary -*.eps binary -*.gif binary -*.gifv binary -*.ico binary -*.jng binary -*.jp2 binary -*.jpg binary -*.jpeg binary -*.jpx binary -*.jxr binary -*.pdf binary -*.png binary -*.psb binary -*.psd binary -*.svgz binary -*.tif binary -*.tiff binary -*.wbmp binary -*.webp binary - -# Fonts -*.ttf binary -*.eot binary -*.otf binary -*.woff binary -*.woff2 binary - -# Archives -*.7z binary -*.gz binary -*.tar binary -*.tgz binary -*.zip binary - -# Audio -*.kar binary -*.m4a binary -*.mid binary -*.midi binary -*.mp3 binary -*.ogg binary -*.ra binary - -# Video -*.3gpp binary -*.3gp binary -*.as binary -*.asf binary -*.asx binary -*.fla binary -*.flv binary -*.m4v binary -*.mng binary -*.mov binary -*.mp4 binary -*.mpeg binary -*.mpg binary -*.ogv binary -*.swc binary -*.swf binary -*.webm binary diff --git a/conflicts/.github/ISSUE_TEMPLATE/bug_report.yaml b/conflicts/.github/ISSUE_TEMPLATE/bug_report.yaml deleted file mode 100644 index 93c91f3fd..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/bug_report.yaml +++ /dev/null @@ -1,62 +0,0 @@ -name: Bug Report Template -description: Report a bug to help us improve -labels: ["kind/bug"] -body: - - type: textarea - id: description - attributes: - label: Description - What happened? * - description: A clear and concise description of the bug. - placeholder: Tell us what you see! - validations: - required: true - - type: textarea - id: expected - attributes: - label: Expected Behavior * - description: A clear and concise description of what you expected to happen. - placeholder: Tell us what you expected! - validations: - required: true - - type: textarea - id: observed - attributes: - label: Observed Behavior * - description: A clear and concise description of what happened instead. - placeholder: Tell us what you observed! - validations: - required: true - - type: textarea - id: steps - attributes: - label: Steps to Reproduce - description: Steps to reproduce the behavior. - placeholder: Tell us how to reproduce the issue! - validations: - required: false - - type: textarea - id: context - attributes: - label: Context Information - description: Add any other context about the problem here. - validations: - required: false - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - validations: - required: false - - type: textarea - id: screenshots - attributes: - label: Screenshots - description: If applicable, add screenshots or other information to help explain your problem. - validations: - required: false - - type: markdown - attributes: - value: | - _* These fields are mandatory, without filling them it is not possible to create the issue._ diff --git a/conflicts/.github/ISSUE_TEMPLATE/config.yml b/conflicts/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 0086358db..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: true diff --git a/conflicts/.github/ISSUE_TEMPLATE/documentation.md b/conflicts/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 4ca8c166d..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Documentation Update Request -about: Create a report to help us improve our documentation -title: "" -labels: "task/documentation" -assignees: "" ---- - -# Documentation Update Request - -## Description - - -## Current Documentation - - -## Proposed Changes - - -## Justification - - -## Additional Context - - -## Deadline - - -## Notes - diff --git a/conflicts/.github/ISSUE_TEMPLATE/epic_template.md b/conflicts/.github/ISSUE_TEMPLATE/epic_template.md deleted file mode 100644 index 24edb0b59..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/epic_template.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Epic -about: Help us with new ideas -title: "" -labels: "kind/epic" -assignees: "" ---- - -# Epic - -## Description - - -### Requirements - - -## Work Breakdown - - -```[tasklist] -### Stories -- [ ] Create Stories which can be converted into issues -``` - -## Initiative / goal - - -### Hypothesis - - -## Acceptance criteria and must have scope - - -## Stakeholders - - -## Timeline - - -## Need for refinement - diff --git a/conflicts/.github/ISSUE_TEMPLATE/feature_request.md b/conflicts/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 5ff7afa21..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Feature Request -about: Help us with new features -title: "" -labels: "kind/enhancement" -assignees: "" ---- - -# Feature Request - -## Description - -- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. - -## Which Areas Would Be Affected? - - -## Why Is the Feature Desired? - - -## How does this tie into our current product? - - -## Stakeholders - - -## Solution Proposal and Work Breakdown - - -```[tasklist] -- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata -- [ ] Refine a Solution Proposal / Work Breakdown -- [ ] (For Tech Team): Include acceptance criteria for the sub-tasks of the work breakdown -``` diff --git a/conflicts/.github/ISSUE_TEMPLATE/mds_feature_request.md b/conflicts/.github/ISSUE_TEMPLATE/mds_feature_request.md deleted file mode 100644 index 918890e56..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/mds_feature_request.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: MDS Feature Request -about: Help us improve your product experience with new features suggestions -title: "" -labels: ["kind/enhancement", "scope/mds", "status/blocked/needs-product"] -assignees: ["jkbquabeck", "AbdullahMuk"] ---- - -# Feature Request - -## Description - -_A clear and concise description of what the customer wants to happen._ - -- As a USER who PRECONDITIONS, I want to DO_THING, so I can ACCOMPLISH_GOAL. - -## Which Areas Would Be Affected? - -_e.g., DPF, CI, build, transfer, etc._ - -## Why Is the Feature Desired? - -_What problems does that user face that existing functionalities do solve?_ - -## How does this tie into the current product? - -_Describe whether this request is related to an existing workflow, feature, or otherwise something in the product today. Or, does this open us up to new innovative ideas?_ - -## Scope check - -_Is this feature part of the contracted scope?_ -- [ ] It is part of the contracted scope -- [ ] It is not part of the contracted scope - -If not, please add the label "mds/future-scope" - -## (For sovity Team to complete) Stakeholders - -_Add more on who asked for this, i.e. company, person, how much they pay us, what their tier is, are they a strategic account, etc. Who needs to be kept up-to-date about this feature?_ - -## (For sovity Team to complete) Solution Proposal and Work Breakdown - -```[tasklist] -- [ ] Fix the GitHub Projects Labels, Sprint and other Metadata -- [ ] Refine further action items for this feature request -``` - diff --git a/conflicts/.github/ISSUE_TEMPLATE/process.md b/conflicts/.github/ISSUE_TEMPLATE/process.md deleted file mode 100644 index 4957c23cb..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/process.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Refine Process Request -about: Existing processes must be adapted or new ones created -title: "" -labels: "task/documentation" -assignees: "" ---- - -# Process Refinement Request - -## Description - - -## Current State - - -## Proposed Changes - - -## Related Issues or PRs - - -## Additional Information - diff --git a/conflicts/.github/ISSUE_TEMPLATE/release.md b/conflicts/.github/ISSUE_TEMPLATE/release.md index 07fda28e3..6852a9846 100644 --- a/conflicts/.github/ISSUE_TEMPLATE/release.md +++ b/conflicts/.github/ISSUE_TEMPLATE/release.md @@ -26,7 +26,6 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Write or review a release summary. - [ ] Remove empty sections from the patch notes. - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the released edc-extensions version. - - TODO: update links to new .env after the merge - [ ] Set the broker server release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] Set the EDC UI release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] Set the EDC CE release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). @@ -42,8 +41,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Check @jkbquabeck for an up-to-date mailing list, separated into "To" and "Cc". - [ ] Attach the Deployment Docs Zip generated during the GitHub release, which should now contain the CHANGELOG, deployment migration notes, an initial deployment guide and a local demo docker compose. - [ ] Optional, this can be done mid-development if required: - - [ ] Create a `release-cleanup` PR. - - TODO: update links to new .env after the merge + - [ ] Create a `release-cleanup` PR. - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the EDC UI. - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the EDC CE. - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the Broker Server. diff --git a/conflicts/.github/PULL_REQUEST_TEMPLATE.md b/conflicts/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index c777e39aa..000000000 --- a/conflicts/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,10 +0,0 @@ -_What issues does this PR close?_ - - -```[tasklist] -### Checklist -- [ ] The PR title is short and expressive. -- [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/authority-portal/tree/main/docs/dev/changelog_updates.md) for more information. -- [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. -- [ ] I have performed a **self-review** -``` diff --git a/conflicts/.github/workflows/add_issue_to_project.yml b/conflicts/.github/workflows/add_issue_to_project.yml deleted file mode 100644 index 76fd8814c..000000000 --- a/conflicts/.github/workflows/add_issue_to_project.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Add issue to project action - -on: - issues: - types: - - opened - -jobs: - add_issue_to_project: - name: add_issue_to_project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v1.0.1 - with: - project-url: https://github.com/orgs/sovity/projects/9 - github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT_PAT }} diff --git a/conflicts/.github/workflows/build-and-publish-connector-images.yml b/conflicts/.github/workflows/build-and-publish-connector-images.yml deleted file mode 100644 index c443c97cb..000000000 --- a/conflicts/.github/workflows/build-and-publish-connector-images.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: EDC Image CI - -on: - push: - branches: [ main ] - release: - types: [ published ] - pull_request: - branches: [ main ] - -env: - REGISTRY: ghcr.io - IMAGE_NAME_BASE: ${{ github.repository_owner }} - IMAGE_NAME: edc - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - strategy: - matrix: - imageVariants: [ - { - "imageName": "broker-server-dev", - "title": "Broker Server (Dev)", - "description": "EDC IDS Broker Server. This dev version contains no persistence or auth and can be used to quickly start a locally running Broker Server + Broker UI.", - "buildArgs": "-Pdmgmt-api-key" - }, - { - "imageName": "broker-server-ce", - "title": "Broker Server (Community Edition)", - "description": "EDC IDS Broker Server. Contains DB extensions and requires dataspace credentials to join an existing dataspace.", - "buildArgs": "-Pdmgmt-api-key -Pfs-vault -Poauth2" - } - ] - timeout-minutes: 30 - permissions: - contents: read - packages: write - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: edc - POSTGRES_PASSWORD: edc - POSTGRES_DB: edc - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BASE }}/${{ matrix.imageVariants.imageName }} - labels: | - org.opencontainers.image.title=${{ matrix.imageVariants.title }} - org.opencontainers.image.description=${{ matrix.imageVariants.description }} - tags: | - type=schedule - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=ref,event=branch - type=ref,event=pr - type=sha - type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=release,enable=${{ startsWith(github.ref, 'refs/tags/') }} - - name: Build and push EDC image - uses: docker/build-push-action@v5 - with: - file: connector/Dockerfile - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - network: host - build-args: | - USERNAME=${{ github.actor }} - TOKEN=${{ secrets.GITHUB_TOKEN }} - BUILD_ARGS=${{ matrix.imageVariants.buildArgs }} - TEST_POSTGRES_JDBC_URL=jdbc:postgresql://localhost:5432/edc - TEST_POSTGRES_JDBC_USER=edc - TEST_POSTGRES_JDBC_PASSWORD=edc diff --git a/conflicts/.github/workflows/build-and-publish-ts-api-client.yml b/conflicts/.github/workflows/build-and-publish-ts-api-client.yml deleted file mode 100644 index 0b49fab03..000000000 --- a/conflicts/.github/workflows/build-and-publish-ts-api-client.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: TypeScript API Client Library - -on: - push: - branches: [ main ] - release: - types: [ published ] - pull_request: - branches: [ main ] - -jobs: - build-and-publish-npm-package: - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - packages: write - - steps: - - uses: FranzDiebold/github-env-vars-action@v2 - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - uses: actions/setup-node@v4 - with: - node-version: 16 - cache: 'npm' - registry-url: 'https://registry.npmjs.org' - cache-dependency-path: ./extensions/broker-server-api/client-ts/package-lock.json - - name: Generate openapi.yaml & Client Code - run: | - ./gradlew :extensions:broker-server-api:api:clean :extensions:broker-server-api:api:build -x test --no-daemon - env: - USERNAME: ${{ github.actor }} - TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: NPM Package Dist Tag & Version - working-directory: ./extensions/broker-server-api/client-ts - run: | - if [ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]; then - # Full Release - VERSION="${GITHUB_REF#refs/tags/v}" - DIST_TAG=latest - else - VERSION="0.$(date '+%Y%m%d.%H%M%S')-main-$CI_SHA_SHORT" - DIST_TAG=main - fi - npm version $VERSION - echo "DIST_TAG=$DIST_TAG" >> $GITHUB_ENV - - name: Build NPM Library - working-directory: ./extensions/broker-server-api/client-ts - run: | - npm ci && npm run build - - name: Publish NPM Library - if: github.ref == 'refs/heads/main' || github.ref == 'refs/tags/v*' - working-directory: ./extensions/broker-server-api/client-ts - run: | - npm set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN - npm set //registry.npmjs.org/:username $NODE_USER - npm publish --access public --tag "${{ env.DIST_TAG }}" - env: - NODE_USER: richardtreier-sovity - NODE_AUTH_TOKEN: ${{ secrets.SOVITY_BROKER_SERVER_CLIENT_NPM_AUTH }} diff --git a/conflicts/.github/workflows/code_analysis.yml b/conflicts/.github/workflows/code_analysis.yml deleted file mode 100644 index 74afd292f..000000000 --- a/conflicts/.github/workflows/code_analysis.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Code Analysis - -on: - workflow_dispatch: - pull_request: - branches: [main] - paths-ignore: - - "**.md" - - "docs/**" - -jobs: - is_java_project: - runs-on: ubuntu-latest - outputs: - pom_exists: ${{ steps.check_files.outputs.files_exists }} - checkstyle_active: ${{ steps.check_checkstyle.outputs.checkstyle_active }} - spotbugs_active: ${{ steps.check_spotbugs.outputs.spotbugs_active }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Check file existence - id: check_files - uses: andstor/file-existence-action@v2 - with: - files: "pom.xml" - - name: check_checkstyle - id: check_checkstyle - run: echo "checkstyle_active=$(if grep -q "maven-checkstyle-plugin" pom.xml; then echo "true"; else echo "false"; fi)" >> $GITHUB_OUTPUT - - name: check_spotbugs - id: check_spotbugs - run: echo "spotbugs_active=$(if grep -q "spotbugs-maven-plugin" pom.xml; then echo "true"; else echo "false"; fi)" >> $GITHUB_OUTPUT - run_checkstyle: - needs: [is_java_project] - if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.checkstyle_active == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "17" - cache: "maven" - - name: Run style checks - run: mvn -B checkstyle:check --file pom.xml - run_spotbugs: - needs: [is_java_project] - if: needs.is_java_project.outputs.pom_exists == 'true' && needs.is_java_project.outputs.spotbugs_active == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "17" - cache: "maven" - - name: Run static code analysis - run: mvn -B compile spotbugs:check --file pom.xml diff --git a/conflicts/.github/workflows/codeql.yml b/conflicts/.github/workflows/codeql.yml deleted file mode 100644 index 3c6b0894f..000000000 --- a/conflicts/.github/workflows/codeql.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '34 8 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java-kotlin', 'javascript-typescript' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - env: - USERNAME: ${{ github.actor }} - TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/conflicts/.github/workflows/license_scan.yml b/conflicts/.github/workflows/license_scan.yml deleted file mode 100644 index 7a0e07505..000000000 --- a/conflicts/.github/workflows/license_scan.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Trivy License Scan - -on: - push: - -jobs: - license_scan1: - name: License scan (rootfs) - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Run license scanner - uses: aquasecurity/trivy-action@master - with: - scan-type: "rootfs" - scan-ref: "." - scanners: "license" - severity: "CRITICAL,HIGH" - exit-code: 1 - license_scan2: - name: License scan (repo) - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: npm install (client-ts) - run: cd extensions/broker-server-api/client-ts && npm install - - name: Run license scanner - uses: aquasecurity/trivy-action@master - with: - scan-type: "repo" - scan-ref: "." - scanners: "license" - severity: "CRITICAL,HIGH" - exit-code: 1 diff --git a/conflicts/.github/workflows/release_docs_zip.yml b/conflicts/.github/workflows/release_docs_zip.yml deleted file mode 100644 index c248252e0..000000000 --- a/conflicts/.github/workflows/release_docs_zip.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Release Docs Zip File - -on: - release: - types: [ published ] - -env: - IMAGE_NAME_BASE: ${{ github.repository_owner }} - -jobs: - add_docs_zip_to_release: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Archive deployment-relevant documentation. - run: | - ARCHIVE_FILE_NAME="broker-server-release-${GITHUB_REF#refs/tags/v}-deployment-docs.zip" - echo "ARCHIVE_FILE_NAME=$ARCHIVE_FILE_NAME" >> $GITHUB_ENV - zip -r -q $ARCHIVE_FILE_NAME README.md CHANGELOG.md docker-compose.yaml .env connector/.env connector/Dockerfile connector/README.md - - name: Upload deployment-relevant documentation - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ env.ARCHIVE_FILE_NAME }} - tag: ${{ github.ref }} - overwrite: true diff --git a/conflicts/.github/workflows/secret_scan.yml b/conflicts/.github/workflows/secret_scan.yml deleted file mode 100644 index 613fc5682..000000000 --- a/conflicts/.github/workflows/secret_scan.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Trivy Secret Scan - -on: - push: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - secret-scan: - name: secret_scan - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Run vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - scan-type: "fs" - exit-code: "1" - ignore-unfixed: true - scanners: secret diff --git a/conflicts/.github/workflows/security_scan.yml b/conflicts/.github/workflows/security_scan.yml deleted file mode 100644 index 9c3f06aa1..000000000 --- a/conflicts/.github/workflows/security_scan.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Trivy Security Scan - -on: - push: - workflow_dispatch: - -jobs: - security_scan: - name: security_scan - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Run static analysis (rootfs) - uses: aquasecurity/trivy-action@master - with: - scan-type: "rootfs" - scanners: "vuln,misconfig" - ignore-unfixed: true - format: 'table' - severity: "CRITICAL,HIGH" - - name: Run static analysis (repo) - uses: aquasecurity/trivy-action@master - with: - scan-type: "repo" - scanners: "vuln,misconfig" - ignore-unfixed: true - format: 'table' - severity: "CRITICAL,HIGH" diff --git a/conflicts/.pre-commit-README.md b/conflicts/.pre-commit-README.md deleted file mode 100644 index eae74228d..000000000 --- a/conflicts/.pre-commit-README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Pre-Commit-Hook -The defined pre-commit-hook prevents committing passwords to the repository. In case a password is detected -git commit fails. - -## Install pre-commit and detect-secrets -1. Install pre-commit-hook tool - `$ pip install pre-commit` -2. Install detect-secrets - `$ pip install detect-secrets` - -## Enable secret-scanning pre-commit hook -1. Update pre-commit-hook - `$ pre-commit autoupdate` -2. Enable defined pre-commit-hook - `$ pre-commit install` - -## On repository initialization of pre-commit hook with detect-secrets -If no `.secrets.baseline` is present, simply generate it: -1. `$ detect-secrets scan --disable-plugin KeywordDetector --disable-plugin AWSKeyDetector > .secrets.baseline` -2. Use Notepad++ or IntelliJ-Editor to convert `.secrets.baseline` to UTF-8 - -## Add false-positives or force adding secrets -1. `$ detect-secrets scan --baseline .secrets.baseline` -2. If secrets are identified, add them to .secrets.baseline manually -For more details see: https://github.com/Yelp/detect-secrets#adding-secrets-to-baseline diff --git a/conflicts/.pre-commit-config.yaml b/conflicts/.pre-commit-config.yaml deleted file mode 100644 index 5d62bf09d..000000000 --- a/conflicts/.pre-commit-config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -repos: - - repo: https://github.com/Yelp/detect-secrets - rev: v1.4.0 - hooks: - - id: detect-secrets - args: ['--baseline', '.secrets.baseline'] - exclude: package.lock.json \ No newline at end of file diff --git a/conflicts/CHANGELOG.md b/conflicts/CHANGELOG.md deleted file mode 100644 index a22c8a468..000000000 --- a/conflicts/CHANGELOG.md +++ /dev/null @@ -1,645 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - yyyy-mm-dd - -### Overview - -### Detailed Changes - -#### Major - -#### Minor - -#### Patch - -### Deployment Migration Notes - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:{{ CE_VERSION }}` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI_VERSION }}` -- Sovity EDC CE: {{ CE Release Link }} - -## [v4.2.0] - 2024-04-11 - -### Overview - -Bumped EDC CE version to 7.4.2. - -### Detailed Changes - -#### Minor - -- Bumped EDC CE version to 7.4.2. -- Bumped EDC UI version to 3.2.2. -- Better handling of custom properties. - -### Deployment Migration Notes - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.2.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` -- Sovity EDC CE: [`7.4.2`](https://github.com/sovity/edc-extensions/releases/tag/v7.4.2) - -## [v4.1.1] - 2024-04-11 - -### Overview - -Pull changes from EDC UI 3.1.0 into the broker. - -### Detailed Changes - -#### Patch - -- Bump EDC UI version to 3.1.0 - - "Name" column renamed to "Title" - - Fix status icon for data offers - -### Deployment Migration Notes - -_No special deployment migration steps required_ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.1.1` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.1.0` -- Sovity EDC CE: [`7.3.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.3.0) - -## [v4.1.0] - 2024-04-02 - -### Overview - -Pull changes from EDC CE 7.3.0 into the broker. - -### Detailed Changes - -#### Minor - -- Bumped EDC version to 7.3.0: - - Broker UI: Support for UIAsset's `customJsonAsString` and `customJsonLdAsString`, along with their private counterparts. - -### Deployment Migration Notes - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.1.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.0.0` -- Sovity EDC CE: [`7.3.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.3.0) - -## [v4.0.0] - 2024-03-22 - -### Overview - -Release with adjustmets for the ongoing integration with the Authority Portal - -### Detailed Changes - -#### Major - -- Authority Portal API: Removed deprecated data offer count endpoint - -#### Minor - -- API: Added endpoint for adding connectors and associated MDS IDs - -### Deployment Migration Notes - -- Authority Portal API: The deprecated data offer count endpoint was removed: ~~``authority-portal-api/data-offer-counts``~~. - Alternatively the connector metadata endpoint should be used: `authority-portal-api/connectors`. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:4.0.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:3.0.0` -- Sovity EDC CE: [`7.2.2`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.2) - -## [v3.5.0] - 2024-02-29 - -### Overview - -Enable better integration of Broker UI and Authority Portal - -### Detailed Changes - -#### Minor - -- Added query params for the connector endpoints filter - -#### Deployment Migration Notes - -_No special deployment migration steps required_ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.5.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.5.0` -- Sovity EDC CE: [`7.2.1`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.1) - -## [v3.4.0] - 2024-02-27 - -### Overview - -Release to accommodate the Authority Portal release. - -### Detailed Changes - -#### Minor - -- Authority Portal API: Added endpoint for receiving all data offers of registered connectors - -#### Patch - -- Updated dependency version to have stable Policy (and Contract) identifiers. - -### Deployment Migration Notes - -_No special deployment migration steps required_ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.4.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` -- Sovity EDC CE: [`7.2.1`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.1) - -## [v3.3.0] - 2024-02-14 - -### Overview - -MDS bugfix and feature release - -### Detailed Changes - -#### Minor - -- Assets now have new MDS fields - -### Deployment Migration Notes - -_No special deployment migration steps required_ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.3.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.4.0` -- Sovity EDC CE: [`7.2.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.2.0) - -## [v3.2.0] - 2024-01-18 - -### Overview - -Added validated organization information. - -### Detailed Changes - -#### Minor - -- Validated organization information from the Authority Portal is now displayed -- Authority Portal API: Added endpoint for receiving organization metadata - -### Deployment Migration Notes - -_No special deployment migration steps required_ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.2.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.3.1` -- Sovity EDC CE: [`7.1.1`](https://github.com/sovity/edc-extensions/releases/tag/v7.1.1) - -## [v3.1.0] - 2023-08-12 - -### Overview - -Re-added deprecated endpoints for Authority Portal API backward compatibility. - -### Detailed Changes - -#### Minor - -- Authority Portal API: Removed data offer count endpoint in favor of new Connector Metadata Endpoint. - -### Deployment Migration Notes - -_No special migration steps required._ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.1.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.2.0` -- Sovity EDC CE: [`7.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.0.0) - -## [v3.0.0] - 2023-06-12 - -### Overview - -EDC 0 / MDS 2.0 bugfix release, Authority Portal API Connector Metadata Endpoint. - -### Detailed Changes - -#### Major - -- Authority Portal API: Removed data offer count endpoint in favor of new Connector Metadata Endpoint. - -#### Minor - -- Bumped sovity EDC CE to `7.0.0`. -- Bumped Broker UI to `2.2.0`. -- Authority Portal API: Added new Connector Metadata endpoint that includes online status, participant ID and data offer - counts. - -### Deployment Migration Notes - -- The DAPS needs to contain the claim `referringConnector=broker` for the broker. The expected value `broker` could be - overridden by - specifying a different value for `MY_EDC_PARTICIPANT_ID`. -- Authority Portal API: The data offer count endpoint was removed in favor of the new Connector Metadata - Endpoint: `authority-portal-api/connectors`, used to be ~~``authority-portal-api/data-offer-counts``~~. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:3.0.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.2.0` -- Sovity EDC CE: [`7.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v7.0.0) - -## [v2.0.2] - 2023-11-23 - -### Overview - -EDC 0 Bugfix Release. - -### Detailed Changes - -#### Patch - -- Fixed an issue with the healthcheck. - -### Deployment Migration Notes - -_No special migration steps required._ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.2` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` -- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) - -## [v2.0.1] - 2023-11-20 - -### Overview - -EDC 0 Bugfix Release. - -### Detailed Changes - -#### Patch - -- Fixed an issue preventing DAPS roll-in with the `broker-server-ce` variant. - -### Deployment Migration Notes - -_No special migration steps required._ - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.1` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` -- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) - -## [v2.0.0] - 2023-11-17 - -### Overview - -EDC 0 Release, some bugfixes. - -### Detailed Changes - -#### Major - -- Migrated to Eclipse EDC 0.2.1 -- Migrated to edc-extensions 5.0.0 -- Migrated Assets to JSON-LD - -#### Minor - -- New Filter: Organization Name -- Search now hits Organization Name - -#### Patch - -- Fixed some issues with DB Connections not released between tests. -- Fixed issue with initial sorting not being the first sorting. - -### Deployment Migration Notes - -1. Connectors and Data Offers require an initial crawl before their metadata is filled again. -2. UI Migration Notes since the last Broker Release: https://github.com/sovity/edc-ui/releases/tag/v2.0.0 -3. The Protocol Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to - be `https://[MY_EDC_FQDN]/backend/api/v1/ids`~~. -4. The Management Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/management`, ~~used to - be `https://[MY_EDC_FQDN]/backend/api/v1/management`~~. -5. The Connector Endpoint changed to `https://[MY_EDC_FQDN]/backend/api/dsp`, ~~used to - be `https://[MY_EDC_FQDN]/backend/api/v1/ids/data`~~. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:2.0.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:2.1.0` -- Sovity EDC CE: [`6.0.0`](https://github.com/sovity/edc-extensions/releases/tag/v6.0.0) - -## [v1.2.0] - 2023-10-30 - -### Overview - -Adapt to requirements of the Authority Portal - Release v2.0.0. - -### Detailed Changes - -#### Minor - -- Added an endpoint for getting the data offer amounts for connectors. -- Added a Connector filter to the Catalog Page. - -### Deployment Migration Notes - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.2.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` -- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) - -## [v1.1.1] - 2023-10-11 - -### Overview - -Bugfix release for the asset properties issue. - -### Detailed Changes - -#### Patch - -- Fixed a bug causing some string asset properties getting quotes around them. - -### Deployment Migration Notes - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.1.1` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` -- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) - -## [v1.1.0] - 2023-09-29 - -### Overview - -Bugfix release for the asset properties issue. Also contains the connector delete endpoint. - -### Detailed Changes - -#### Minor - -- New Admin API Endpoint: Delete Connectors - -#### Patch - -- Fixed a bug causing exceptions when non-string asset properties were used. - -### Deployment Migration Notes - -1. Connectors can now be dynamically deleted at runtime by using the following endpoint: - ```shell script - # Response should be 204 No Content - curl --request DELETE \ - --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ - --header 'Content-Type: application/json' \ - --header 'x-api-key: ApiKeyDefaultValue' \ - --data '["https://some-connector-to-delete/api/dsp", "https://some-other-connector-to-delete/api/dsp"]' - ``` - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.1.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` -- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) - -## [v1.0.3] - 2023-09-01 - -### Overview - -Bugfix Release for the Broker MvP with MS8. - -### Detailed Changes - -#### Patch - -- Fixed sorting the catalog by popularity. - -### Deployment Migration Notes - -No configuration changes are required. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.3` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity13` -- Sovity EDC CE: [`4.2.0`](https://github.com/sovity/edc-extensions/tree/v4.2.0/connector) - -## [v1.0.2] - 2023-08-10 - -### Overview - -Bugfix Release for the Broker MvP with MS8. - -### Detailed Changes - -#### Patch - -- Fixed an issue where connector crawling failed when data offer limits were exceeded. -- Fixed searching data offers with capital letters didn't work. - -### Deployment Migration Notes - -No configuration changes are required. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.2` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` -- Sovity EDC CE: [`4.1.0`](https://github.com/sovity/edc-extensions/tree/v4.1.0/connector) - -## [v1.0.1] Broker MvP Bugfix / Feature Release - 2023-07-12 - -### Overview - -Bugfix / Feature Release for the Broker MvP with MS8: Connectors can now be added at runtime. - -### Detailed Changes - -#### Major - -- Broker Server API now generates into its own Broker Server Client Typescript Library. - -#### Minor - -- Broker Server API is now part of this repository. -- Dead Connectors are now deleted periodically. -- Connector Online Status is now visualized. -- New Admin API Endpoint: Add Connectors - -#### Patch - -- Fixed Backend Docker Healthcheck - -### Deployment Migration Notes - -1. Added new **required** configuration properties: - ```yaml - # Broker Server Admin Api Key (required) - # This is a stopgap until we have IAM - EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey - ``` -2. Added new **optional** configuration properties: - ```yaml - # CRON interval for crawling ONLINE connectors - EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH: "*/20 * * ? * *" # every 20s - - # CRON interval for crawling OFFLINE connectors - EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH: "0 */5 * ? * *" # every 5 minutes - - # CRON interval for crawling DEAD connectors - EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH: "0 0 * ? * *" # every hour - - # CRON interval for marking connectors as DEAD - EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS: "0 0 2 ? * *" # every day at 2am - - # Delete data offers / mark as dead after connector has been offline for: - EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER: "P5D" - - # Hide data offers after connector has been offline for: - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "P1D" - ``` -3. Removed **optional** configuration properties: - ```yaml - # (Removed) CRON interval for crawling connectors - EDC_BROKER_SERVER_CRON_CONNECTOR_REFRESH: "0 */5 * ? * *" - ``` -4. Connectors can now be dynamically added at runtime by using the following endpoint: - ```shell script - # Response should be 204 No Content - curl --request PUT \ - --url 'http://localhost:11002/backend/api/v1/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ - --header 'Content-Type: application/json' \ - --header 'x-api-key: ApiKeyDefaultValue' \ - --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' - ``` - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:1.0.1` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity12` -- Sovity EDC CE: [`4.0.1`](https://github.com/sovity/edc-extensions/tree/v4.0.1/connector) - -## [v1.0.0] - -Release was deleted in favor of above release. There was a bug, and we just decided to re-do the release. - -## [v0.1.0] Broker MvP Release - 2023-06-23 - -### Overview - -Broker MvP using Core EDC MS8. - -### Detailed Changes - -#### Minor - -- Implemented Catalog Page Filters: - - Data Space Filter - - Data Category - - Data Subcategory - - Data Model - - Transport Mode - - Geo Reference Method -- Implemented Catalog Page Sorting: - - Most Recent - - By Title - - By Connector -- Implemented Catalog Page Pagination. - -#### Patch - -- Fix: Data Offer Filter available values are no longer limited to the selected value if a value is selected. -- Fix: Missing file system vault prevented data space login. -- Fix: Parallel crawling was not actually parallel - -### Deployment Migration Notes - -1. There are new **required** configuration properties: - ```yaml - # List of Data Space Names for special Connectors (default: '') - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/api/dsp,OtherDataspace=https://some-other-connector/api/dsp" - ``` -2. There are new **optional** configuration properties available for overriding: - ```yaml - # Parallelization for Crawling (default: 3) - EDC_BROKER_SERVER_NUM_THREADS: 16 - - # Default Data Space Name (default: MDS) - EDC_BROKER_SERVER_DEFAULT_DATASPACE: MDS - - # Maximum number of Data Offers per Connector (default: 50) - EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR: 50 - - # Maximum number of Contract Offers per Data Offer (default: 10) - EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER: 10 - - # Pagination Configuration: Catalog Page Size (default: 20) - EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE: 20 - - # Database Connection Pool Size - EDC_BROKER_SERVER_DB_CONNECTION_POOL_SIZE: 30 - - # Database Connection Timeout (in ms) - EDC_BROKER_SERVER_DB_CONNECTION_TIMEOUT_IN_MS: 30000 - ``` -3. An issue prevented the keystore file from being read, preventing a successful data space log in. -4. Added a reference to [connector/.env](.) as source for other possible broker server configuration - options, that have defaults, but might have use cases for overriding. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.1.0` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity8` -- Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) - -## [v0.0.1] Broker PoC Release - 2023-06-02 - -### Overview - -Initial Broker PoC Release with a minimalistic feature set. - -### Detailed Changes - -#### Major - -- Implemented a Broker PoC with EDC MS8: - - Periodic Crawling of Connectors - - Query Data Offers via UI - - Query Connectors via UI - - Persistence of Connector Status Updates - - Persistence of Crawling Execution Times - -### Deployment Migration Notes - -Please view the [Deployment Section in the README.md](README.md#deployment) for initial deployment instructions. - -#### Compatible Versions - -- Broker Backend Docker Image: `ghcr.io/sovity/broker-server-ce:0.0.1` -- Broker UI Docker Image: `ghcr.io/sovity/edc-ui:0.0.1-milestone-8-sovity6` -- Sovity EDC CE: [`3.3.0`](https://github.com/sovity/edc-extensions/tree/v3.3.0/connector) diff --git a/conflicts/CODE_OF_CONDUCT.md b/conflicts/CODE_OF_CONDUCT.md deleted file mode 100644 index dc748a7ef..000000000 --- a/conflicts/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,70 +0,0 @@ -# Code of Conduct -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), -version 1.4, available at http://contributor-covenant.org/version/1/4. diff --git a/conflicts/CONTRIBUTING.md b/conflicts/CONTRIBUTING.md deleted file mode 100644 index cf973be5a..000000000 --- a/conflicts/CONTRIBUTING.md +++ /dev/null @@ -1,162 +0,0 @@ -# Contributing to the Project - -Thank you for your interest in contributing to this project - -## Table of Contents - -* [Code Of Conduct](#code-of-conduct) -* [How to Contribute](#how-to-contribute) - * [Discuss](#discuss) - * [Create an Issue](#create-an-issue) - * [Submit a Pull Request](#submit-a-pull-request) - * [Report on Flaky Tests](#report-on-flaky-tests) -* [Etiquette for pull requests](#etiquette-for-pull-requests) -* [Contact Us](#contact-us) - -## Code Of Conduct - -See the [Code Of Conduct](CODE_OF_CONDUCT.md). - -## How to Contribute - -### Discuss - -If you want to share an idea to further enhance the project or discuss potential use cases, please feel free to create a -discussion at the `GitHub Discussions page`] -If you feel there is a bug or an issue, contribute to the discussions in `existing issues` -otherwise [create a new issue](#create-an-issue). - -### Create an Issue - -If you have identified a bug or want to formulate a working item that you want to concentrate on, feel free to create a -new issue at our project's corresponding `GitHub Issues page`. Before doing so, please consider searching for -potentially suitable `existing issues`. - -We also -use [GitHub's default label set](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) -extended by custom ones to classify issues and improve findability. - -If an issue appears to cover changes that will have a (huge) impact on the code base and needs to -first be discussed, or if you just have a question regarding the usage of the software, please -create a `discussion` before raising an issue. - -Please note that if an issue covers a topic or the response to a question that may be interesting -for other developers or contributors, or for further discussions, it should be converted to a -discussion and not be closed. - -### Adhere to Coding Style Guide - -We aim for a coherent and consistent code base, thus the coding style detailed in the [styleguide](STYLEGUIDE.md) should -be followed. - -### Submit a Pull Request - -We would appreciate if your pull request applies to the following points: - -* Conform to following [Etiquette for pull requests](#etiquette-for-pull-requests): - -* Make sure to adjust copyright headers appropriately. - -* The git commit messages should comply to the following format: - ``` - (): - ``` - - Use the [imperative mood](https://github.com/git/git/blob/master/Documentation/SubmittingPatches) - as in "Fix bug" or "Add feature" rather than "Fixed bug" or "Added feature" and - [mention the GitHub issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - e.g. `chore(transfer process): improve logging`. - -* Add meaningful tests to verify your submission acts as expected. - -* Where code is not self-explanatory, add documentation providing extra clarification. - -* PR descriptions should use the current [PR template](.github/PULL_REQUEST_TEMPLATE.md) - -* Submit a draft pull request at early-stage and add people previously working on the same code as - reviewer. Make sure automatic checks pass before marking it as "ready for review": - - * _Continuous Integration_ performing various test conventions. - -### Report on Flaky Tests - -If you discover a randomly failing ("flaky") test, please take the time to check whether an issue for that already -exists and if not, create an issue yourself, providing meaningful description and a link to the failing run. Please also -label it with `Bug` and `github`. Then assign it to whoever was the original author of the relevant piece of code or -whoever worked on it last. If assigning the issue is not possible due to missing rights, please just comment and -@mention the author/last editor. - -Please do not just restart the run, as this would overwrite the results. If you need to, a better way of doing this is -to push an empty commit. This will trigger another run. - -```bash -git commit --allow-empty -m "trigger CI" && git push -``` - -If an issue labeled with `Bug` and `github` is assigned to you, please prioritize addressing this issue as other -people will be affected. -We are taking the quality of our code very serious and reporting on flaky tests is an important step toward improvement -in that area. - -## Etiquette for pull requests - -### As an author - -Submitting pull requests should be done while adhering to a couple of simple rules. - -- Familiarize yourself with [coding style](STYLEGUIDE.md), architectural patterns and other contribution guidelines. -- No surprise PRs please. Before you submit a PR, open a discussion or an issue outlining your planned work and give - people time to comment. It may even be advisable to contact committers using the `@mention` feature. Unsolicited PRs - may get ignored or rejected. -- Create focused PRs: your work should be focused on one particular feature or bug. Do not create broad-scoped PRs that - solve multiple issues as reviewers may reject those PR bombs outright. -- Provide a clear description and motivation in the PR description in GitHub. This makes the reviewer's life much - easier. It is also helpful to outline the broad changes that were made, e.g. "Changes the schema of XYZ-Entity: - the `age` field changed from `long` to `String`". -- If you introduce new 3rd party dependencies, be sure to note them in the PR description and explain why they are - necessary. -- Stick to the established code style, please refer to the [styleguide document](STYLEGUIDE.md). -- All tests should be green, especially when your PR is in `"Ready for review"` -- Mark PRs as `"Ready for review"` only when you're prepared to defend your work. By that time you have completed your - work and shouldn't need to push any more commits other than to incorporate review comments. -- Merge conflicts should be resolved by squashing all commits on the PR branch, rebasing onto `main` and - force-pushing. Do this when your PR is ready to review. -- If you require a reviewer's input while it's still in draft, please contact the designated reviewer using - the `@mention` feature and let them know what you'd like them to look at. -- Re-request reviews after all remarks have been adopted. This helps reviewers track their work in GitHub. -- If you disagree with a committer's remarks, feel free to object and argue, but if no agreement is reached, you'll have - to either accept the decision or withdraw your PR. -- Be civil and objective. No foul language, insulting or otherwise abusive language will be tolerated. -- The PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). - - The title must follow the format as `(): `. - `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test` are allowed for - the ``. - - The length must be kept under 80 characters. - -### As a reviewer - -- Have a look at [Pull Request Review Pyramide](https://www.morling.dev/blog/the-code-review-pyramid/) -- Please complete reviews within two business days or delegate to another committer, removing yourself as a reviewer. -- If you have been requested as reviewer, but cannot do the review for any reason (time, lack of knowledge in particular - area, etc.) please comment that in the PR and remove yourself as a reviewer, suggesting a stand-in. -- Don't be overly pedantic. -- Don't argue basic principles (code style, architectural decisions, etc.) -- Use the `suggestion` feature of GitHub for small/simple changes. -- The following could serve you as a review checklist: - - no unnecessary dependencies in `build.gradle.kts` - - sensible unit tests, prefer unit tests over integration tests wherever possible (test runtime). Also check the - usage of test tags. - - code style - - simplicity and "uncluttered-ness" of the code - - overall focus of the PR -- Don't just wave through any PR. Please take the time to look at them carefully. -- Be civil and objective. No foul language, insulting or otherwise abusive language will be tolerated. The goal is to - _encourage_ contributions. - -## Contact Us - -If you have questions or suggestions, do not hesitate to contact the project developers via https://github.com/sovity. - -## Attribution - -This file is adapted from the [eclipse-edc](https://github.com/eclipse-dataspaceconnector/DataSpaceConnector) project. diff --git a/conflicts/LICENSE.md b/conflicts/LICENSE.md deleted file mode 100644 index 8dc66084c..000000000 --- a/conflicts/LICENSE.md +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2023 sovity.de - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/conflicts/SECURITY.md b/conflicts/SECURITY.md deleted file mode 100644 index b412ae0ef..000000000 --- a/conflicts/SECURITY.md +++ /dev/null @@ -1,32 +0,0 @@ -## Security - -sovity GmbH takes the security of its software products and services seriously, which includes all source code repositories managed through our GitHub organization: [sovity](https://github.com/sovity). - -If you believe you have found a security vulnerability in any of sovity's owned repositories, please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them via mail: [security@sovity.de](mailto:security@sovity.de) - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -## Preferred Languages - -We prefer all communications to be in English. - -## Attribution -This file is adapted from [eclipse-edc](https://github.com/eclipse-edc/DataDashboard) project. diff --git a/conflicts/STYLEGUIDE.md b/conflicts/STYLEGUIDE.md deleted file mode 100644 index bcc9681c1..000000000 --- a/conflicts/STYLEGUIDE.md +++ /dev/null @@ -1,58 +0,0 @@ -# Code Style Guide - -In order to maintain a coherent code style throughout the project we ask every contributor to adhere to a few simple -style guidelines. We assume most developers will use at least something like `IntelliJ` and therefore have support for -automatic code formatting, we are not going to list the guidelines here. If you absolutely want to take a look, checkout -the [config written in XML](.) TODO: fix url after merge - -## Checkstyle configuration - -Checkstyle is a [tool](https://checkstyle.sourceforge.io/) that can statically analyze your source code to check against -a set of given rules. Those rules are formulated in an [XML document](.) . Many modern -IDEs have a plugin available for download that runs in the background and does code analysis. - -This checkstyle config is based off of the [Google Style](https://checkstyle.sourceforge.io/google_style.html) with a -few -additional rules such as the naming of constants and Types. - -_Note: currently we do **not** enforce the generation of Javadoc comments, even though documenting code is **highly** -recommended. We might enable this in the future, such that at least interfaces and public methods are commented._ - -## Running Checkstyle - -In order to get better usability and on-the-fly reporting, Checkstyle is available as IDE plugins for many modern IDEs, -and it can run either on-demand or continuously in the background: - -- [IntelliJ IDEA plugin [recommended]](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) -- [Eclipse IDE [recommended]](https://checkstyle.org/eclipse-cs/#!/) - -### Checkstyle as PR validation - -Apart from running Checkstyle locally as IDE plugin, we do run it on -our [Github Actions pipeline](.github/workflows/code_analysis.yml). At this time, Checkstyle will only spew out warnings, but -we may tighten the rules at a future time and without notice. This will result in failing Github Action pipelines. Also, -committers might reject PRs due to Checkstyle warnings. - -It is therefore **highly** recommended running Checkstyle locally as well. - -If you **do not wish** to run Checkstyle on you local machine, that's fine, but be prepared to get your PRs rejected -simply because of a stupid naming or formatting error. - -> _Note: we do not use the Checkstyle Gradle Plugin on Github Actions because violations would cause builds to fail. For -now, we only want to log warnings._ - -## [Recommended] IntelliJ Code Style Configuration - -If you are using Jetbrains IntelliJ IDEA, we have created a specific code style configuration that will automatically -format your source code according to that style guide. This should eliminate most of the potential Checkstyle violations -right from the get-go. You will need to reformat your code manually or in a pre-commit hook though. - -## [Optional] Generic `.editorconfig` - -For most other editors and IDEs we've supplied an [.editorconfig](.editorconfig) file that can be -placed at the appropriate location. The specific location will largely depend on your editor and your OS, please refer -to the [official documentation](https://editorconfig.org) for details. - -## Attribution - -This file is adapted from the [eclipse-edc](https://github.com/eclipse-dataspaceconnector/DataSpaceConnector) project. diff --git a/conflicts/build.gradle.kts b/conflicts/build.gradle.kts deleted file mode 100644 index e3c232a72..000000000 --- a/conflicts/build.gradle.kts +++ /dev/null @@ -1,86 +0,0 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent - -plugins { - id("java") - id("checkstyle") - id("maven-publish") -} - -dependencies { - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") -} - -val downloadArtifact: Configuration by configurations.creating { - isTransitive = false -} - -allprojects { - apply(plugin = "java") - apply(plugin = "checkstyle") - - tasks.withType { - options.encoding = "UTF-8" - sourceCompatibility = JavaVersion.VERSION_17.toString() - targetCompatibility = JavaVersion.VERSION_17.toString() - } - - tasks.getByName("test") { - useJUnitPlatform() - testLogging { - events = setOf(TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - showExceptions = true - showCauses = true - } - failFast = true - } - - checkstyle { - toolVersion = "9.0" - configFile = rootProject.file("docs/dev/checkstyle/checkstyle-config.xml") - configDirectory.set(rootProject.file("docs/dev/checkstyle")) - maxErrors = 0 // does not tolerate errors - } - - repositories { - mavenCentral() - mavenLocal() - maven { - url = uri("https://oss.sonatype.org/content/repositories/snapshots/") - } - maven { - name = "Github-EDC-Extensions" - url = uri("https://maven.pkg.github.com/sovity/edc-extensions") - credentials { - username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") - password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") - } - } - } -} - -subprojects { - apply(plugin = "maven-publish") - - val sovityBrokerServerGroup: String by project - val sovityBrokerServerVersion: String by project - - - group = sovityBrokerServerGroup - version = sovityBrokerServerVersion - - publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/sovity/edc-broker-server-extension") - credentials { - username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") - password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") - } - } - } - } -} diff --git a/conflicts/docker-compose.yaml b/conflicts/docker-compose.yaml deleted file mode 100644 index 5143f2ba1..000000000 --- a/conflicts/docker-compose.yaml +++ /dev/null @@ -1,113 +0,0 @@ -version: "3.8" -services: - broker-ui: - image: ${EDC_UI_IMAGE} - ports: - - '11000:8080' - environment: - EDC_UI_ACTIVE_PROFILE: broker - EDC_UI_MANAGEMENT_API_URL: http://localhost:11002/backend/api/management - EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue - NGINX_ACCESS_LOG: off - broker: - image: ${BROKER_IMAGE} - depends_on: - - broker-postgresql - - connector - environment: - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://connector:11003/api/dsp" - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" - - # Hide offline data offers after 1 minute in dev - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" - - MY_EDC_FQDN: "broker" - EDC_API_AUTH_KEY: ApiKeyDefaultValue - - MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc - MY_EDC_JDBC_USER: edc - MY_EDC_JDBC_PASSWORD: edc - - # docker compose local dev environment overrides (don't use with non-dev images) - MY_EDC_PROTOCOL: "http://" - EDC_DSP_CALLBACK_ADDRESS: http://broker:11003/backend/api/dsp - EDC_WEB_REST_CORS_ENABLED: 'true' - EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' - EDC_WEB_REST_CORS_ORIGINS: '*' - EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work - ports: - - '11001:11001' - - '11002:11002' - - '11003:11003' - - '11004:11004' - - '11005:5005' - broker-postgresql: - image: docker.io/bitnami/postgresql:15 - restart: always - environment: - POSTGRESQL_USERNAME: edc - POSTGRESQL_PASSWORD: edc - POSTGRESQL_DATABASE: edc - ports: - - '54321:5432' - volumes: - - 'broker-postgresql:/bitnami/postgresql' - connector-ui: - image: ${EDC_UI_IMAGE} - ports: - - '22000:8080' - environment: - EDC_UI_ACTIVE_PROFILE: mds-open-source - EDC_UI_CONFIG_URL: edc-ui-config - EDC_UI_MANAGEMENT_API_URL: http://localhost:22002/api/management - EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue - NGINX_ACCESS_LOG: off - connector: - image: ${EDC_IMAGE} - depends_on: - - connector-postgresql - environment: - MY_EDC_PARTICIPANT_ID: "MDSL00001XX.C0001XX" - MY_EDC_TITLE: "EDC Connector" - MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" - MY_EDC_CURATOR_URL: "https://example.com" - MY_EDC_CURATOR_NAME: "Example GmbH" - MY_EDC_MAINTAINER_URL: "https://sovity.de" - MY_EDC_MAINTAINER_NAME: "sovity GmbH" - - MY_EDC_FQDN: "connector" - EDC_API_AUTH_KEY: ApiKeyDefaultValue - - MY_EDC_JDBC_URL: jdbc:postgresql://connector-postgresql:5432/edc - MY_EDC_JDBC_USER: edc - MY_EDC_JDBC_PASSWORD: edc - - # docker compose local dev environment overrides (don't use with non-dev images) - MY_EDC_PROTOCOL: "http://" - EDC_DSP_CALLBACK_ADDRESS: http://connector:11003/api/dsp - EDC_WEB_REST_CORS_ENABLED: 'true' - EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' - EDC_WEB_REST_CORS_ORIGINS: '*' - EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work - ports: - - '22001:11001' - - '22002:11002' - - '22003:11003' - - '22004:11004' - - '22005:5005' - connector-postgresql: - image: docker.io/bitnami/postgresql:15 - restart: always - environment: - POSTGRESQL_USERNAME: edc - POSTGRESQL_PASSWORD: edc - POSTGRESQL_DATABASE: edc - ports: - - '54322:5432' - volumes: - - 'connector-postgresql:/bitnami/postgresql' -volumes: - broker-postgresql: - driver: local - connector-postgresql: - driver: local diff --git a/conflicts/docs/dev/changelog_updates.md b/conflicts/docs/dev/changelog_updates.md deleted file mode 100644 index d4e7fcfe7..000000000 --- a/conflicts/docs/dev/changelog_updates.md +++ /dev/null @@ -1,60 +0,0 @@ -Updating the Changelog -====================== - -This project uses a [CHANGELOG.md](../../CHANGELOG.md). - -## Structure of the Changelog - -Each pull request should also update the "Unreleased" section of the changelog. -It should also update the "Deployment Migration Notes" Section of the unreleased section as preparation for the release. - -For each release there will be a separate section especially with an "Overview" section containing a summary -from a product perspective. - -Releases will especially contain a "Compatible Versions" section with the final docker -images and versions of other software components that are connected by APIs. - -## How to categorize a change - -The changelog uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Changes are categorized as either Major, Minor or Patch Changes. - -For this project, changes are categorized as the following: - -### Major Changes - -Major changes include: - -- UX / Product overhauls. -- Breaking Changes in Connector-To-Connector communication -- Breaking Changes to the required deployment units. -- Breaking Changes in APIs for third party applications. - -### Minor Changes - -Minor changes include: - -- New or changed features from a customer perspective. -- New APIs with API contracts with other deployment units (our UI doesn't count). -- New Product Documentation - -### Patch Changes - -Patch changes are basically everything else, that does not add, change or remove any product or external API features. - -- Product Fixes, Bugfixes, Refactorings -- Changes to existing Product Documentation -- New or changes to existing Developer Documentation -- Everything else - -## Released Versions - -On releases the "Unreleased" section is emptied in favor of a new section for the release. - -Whether a release will bump the major, minor or patch version is decided by the unreleased changes in the changelog. - -The Release sections will be cleaned up on release, improved with additional information and made -useful for the customer and people deploying the application, containing both product changes and -deployment migration notes. - -More on that can be found in the [Release Issue Template](../../.github/ISSUE_TEMPLATE/release.md). diff --git a/conflicts/docs/dev/checkstyle/checkstyle-config.xml b/conflicts/docs/dev/checkstyle/checkstyle-config.xml deleted file mode 100644 index 4d9976e07..000000000 --- a/conflicts/docs/dev/checkstyle/checkstyle-config.xml +++ /dev/null @@ -1,416 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/conflicts/gradle.properties b/conflicts/gradle.properties deleted file mode 100644 index 3d66ee0fa..000000000 --- a/conflicts/gradle.properties +++ /dev/null @@ -1,26 +0,0 @@ -# Broker -sovityBrokerServerGroup=de.sovity.broker -sovityBrokerServerVersion=0.0.1-SNAPSHOT - -# Sovity EDC Extensions (for common api model) -sovityEdcExtensionsVersion=7.4.2 -sovityEdcExtensionGroup=de.sovity.edc.ext -sovityEdcGroup=de.sovity.edc - -# Eclipse EDC -edcGroup=org.eclipse.edc -edcVersion=0.2.1 - -# Other Dependencies -assertj=3.23.1 -jupiterVersion=5.8.2 -mockitoVersion=4.8.0 -okHttpVersion=4.10.0 -jsonVersion=20220924 -restAssured=4.5.0 -flywayVersion=9.0.1 -postgresVersion=42.4.0 -testcontainersVersion=1.17.6 - -# Other Properties -org.gradle.jvmargs=-Xmx1024m diff --git a/conflicts/gradle/wrapper/gradle-wrapper.properties b/conflicts/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 41dfb8790..000000000 --- a/conflicts/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/conflicts/gradlew b/conflicts/gradlew deleted file mode 100755 index 1b6c78733..000000000 --- a/conflicts/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/conflicts/gradlew.bat b/conflicts/gradlew.bat deleted file mode 100644 index 107acd32c..000000000 --- a/conflicts/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/conflicts/settings.gradle.kts b/conflicts/settings.gradle.kts deleted file mode 100644 index bf9727aed..000000000 --- a/conflicts/settings.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -rootProject.name = "edc-broker-server-extension" - -include(":extensions:broker-server") -include(":extensions:broker-server-postgres-flyway-jooq") -include(":extensions:broker-server-api:api") -include(":extensions:broker-server-api:client") -include(":connector") diff --git a/connector/Dockerfile b/connector/Dockerfile deleted file mode 100644 index 71205d0d6..000000000 --- a/connector/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -# TODO https://github.com/sovity/edc-broker-server-extension/issues/425 -USER fixme_and_stop_using_root - -FROM gradle:7-jdk17-alpine AS build - -ARG USERNAME -ARG TOKEN -ARG BUILD_ARGS -ARG TEST_POSTGRES_JDBC_URL -ARG TEST_POSTGRES_JDBC_USER -ARG TEST_POSTGRES_JDBC_PASSWORD - -ENV USERNAME=$USERNAME -ENV TOKEN=$TOKEN - -ENV SKIP_TESTCONTAINERS=true -ENV TEST_POSTGRES_JDBC_URL=$TEST_POSTGRES_JDBC_URL -ENV TEST_POSTGRES_JDBC_USER=$TEST_POSTGRES_JDBC_USER -ENV TEST_POSTGRES_JDBC_PASSWORD=$TEST_POSTGRES_JDBC_PASSWORD - -COPY --chown=gradle:gradle . /home/gradle/project/ -WORKDIR /home/gradle/project/ -RUN --mount=type=cache,target=/home/gradle/.gradle/caches gradle build --no-daemon $BUILD_ARGS - -FROM eclipse-temurin:17-jre-alpine - -# TODO https://github.com/sovity/edc-broker-server-extension/issues/425 -USER fixme_and_stop_using_root - -# Optional JVM arguments, such as memory settings -ARG JVM_ARGS="" - -# Install curl for healthcheck and create an empty properties file as migitation for a core EDC issue -RUN apk add --no-cache curl bash && touch /emtpy-properties-file.properties -SHELL ["/bin/bash", "-c"] - -WORKDIR /app - -COPY --from=build /home/gradle/project/connector/build/libs/app.jar /app -COPY ./connector/src/main/resources/logging.properties /app - -# health status is determined by the availability of the /health endpoint -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11001/backend/api/check/health - -# Use "exec" for graceful termination (SIGINT) to reach JVM. -# ARG can not be used in ENTRYPOINT so storing values in ENV variables -ENV JVM_ARGS=$JVM_ARGS - -# Read ENV Vars from .env with substitution -COPY ./connector/.env /app/.env - -# Replaces ENV Var statements so they don't overwrite existing ENV Vars -RUN sed -ri 's/^\s*(\S+)=(.*)$/\1=${\1:-"\2"}/' .env -ENTRYPOINT set -a && source /app/.env && set +a && exec java -Djava.util.logging.config.file=/app/logging.properties $JVM_ARGS -jar app.jar diff --git a/connector/build.gradle.kts b/connector/build.gradle.kts deleted file mode 100644 index 81e0dd58e..000000000 --- a/connector/build.gradle.kts +++ /dev/null @@ -1,43 +0,0 @@ -plugins { - `java-library` - id("application") - id("com.github.johnrengelman.shadow") version "7.1.2" -} - -val edcVersion: String by project -val edcGroup: String by project - -dependencies { - // Control-Plane - implementation("${edcGroup}:control-plane-core:${edcVersion}") - implementation("${edcGroup}:data-plane-selector-core:${edcVersion}") - implementation("${edcGroup}:api-observability:${edcVersion}") - implementation("${edcGroup}:configuration-filesystem:${edcVersion}") - implementation("${edcGroup}:control-plane-aggregate-services:${edcVersion}") - implementation("${edcGroup}:http:${edcVersion}") - implementation("${edcGroup}:dsp:${edcVersion}") - implementation("${edcGroup}:json-ld:${edcVersion}") - - // JDK Logger - implementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") - - // Broker Server + PostgreSQL + Flyway - implementation(project(":extensions:broker-server")) - - // Optional: Connector-To-Connector IAM - if (project.hasProperty("oauth2")) { - implementation("${edcGroup}:vault-filesystem:${edcVersion}") - implementation("${edcGroup}:oauth2-core:${edcVersion}") - } else { - implementation("${edcGroup}:iam-mock:${edcVersion}") - } -} - -application { - mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") -} - -tasks.withType { - mergeServiceFiles() - archiveFileName.set("app.jar") -} diff --git a/connector/src/main/resources/logging.properties b/connector/src/main/resources/logging.properties deleted file mode 100644 index 17dfd8a75..000000000 --- a/connector/src/main/resources/logging.properties +++ /dev/null @@ -1,8 +0,0 @@ -handlers = java.util.logging.ConsoleHandler -.level = INFO -java.util.logging.ConsoleHandler.level = ALL -java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter -java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s %6$s%n -org.eclipse.dataspaceconnector.level = FINE -org.eclipse.dataspaceconnector.handler = java.util.logging.ConsoleHandler -org.eclipse.edc.api.observability.ObservabilityApiController.level = ERROR diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index ac61499e8..a4eb98afd 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -4,7 +4,7 @@ services: image: ${EDC_UI_IMAGE} ports: - '11000:8080' - - '33005:5005' + - '11015:5005' environment: EDC_UI_ACTIVE_PROFILE: ${EDC_UI_ACTIVE_PROFILE} EDC_UI_CONFIG_URL: edc-ui-config @@ -17,7 +17,8 @@ services: edc: image: ${EDC_IMAGE} depends_on: - - postgresql + postgresql: + condition: service_healthy environment: MY_EDC_PARTICIPANT_ID: "my-edc" MY_EDC_TITLE: "EDC Connector" @@ -63,7 +64,8 @@ services: edc2: image: ${EDC_IMAGE} depends_on: - - postgresql2 + postgresql2: + condition: service_healthy environment: MY_EDC_PARTICIPANT_ID: "my-edc2" MY_EDC_TITLE: "EDC Connector 2" @@ -94,8 +96,60 @@ services: - '22004:11004' - '22005:5005' + test-backend: + image: ${TEST_BACKEND_IMAGE} + ports: + - '33001:11001' + + broker-ui: + image: ${EDC_UI_IMAGE} + ports: + - '44000:8080' + environment: + EDC_UI_ACTIVE_PROFILE: broker + EDC_UI_MANAGEMENT_API_URL: http://localhost:44002/backend/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + NGINX_ACCESS_LOG: off + + broker: + image: ${BROKER_IMAGE} + depends_on: + broker-postgresql: + condition: service_healthy + edc: + condition: service_started + edc2: + condition: service_started + environment: + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://edc:11003/api/dsp,http://edc2:11003/api/dsp" + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" + + # Hide offline data offers after 1 minute in dev + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" + + MY_EDC_FQDN: "broker" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://broker:44003/backend/api/dsp + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: + - '44001:11001' + - '44002:11002' + - '44003:11003' + - '44004:11004' + - '44005:5005' + postgresql: - image: docker.io/bitnami/postgresql:11 + image: docker.io/bitnami/postgresql:15 restart: always environment: POSTGRESQL_USERNAME: edc @@ -107,7 +161,7 @@ services: - 'postgresql:/bitnami/postgresql' postgresql2: - image: docker.io/bitnami/postgresql:11 + image: docker.io/bitnami/postgresql:15 restart: always environment: POSTGRESQL_USERNAME: edc @@ -118,13 +172,22 @@ services: volumes: - 'postgresql2:/bitnami/postgresql' - test-backend: - image: ${TEST_BACKEND_IMAGE} + broker-postgresql: + image: docker.io/bitnami/postgresql:15 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc ports: - - '33001:11001' + - '54323:5432' + volumes: + - 'broker-postgresql:/bitnami/postgresql' volumes: postgresql: driver: local postgresql2: driver: local + broker-postgresql: + driver: local diff --git a/docker-compose.yaml b/docker-compose.yaml index ac61499e8..c776e7f9e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: image: ${EDC_UI_IMAGE} ports: - '11000:8080' - - '33005:5005' + - '11015:5005' environment: EDC_UI_ACTIVE_PROFILE: ${EDC_UI_ACTIVE_PROFILE} EDC_UI_CONFIG_URL: edc-ui-config @@ -17,7 +17,8 @@ services: edc: image: ${EDC_IMAGE} depends_on: - - postgresql + postgresql: + condition: service_healthy environment: MY_EDC_PARTICIPANT_ID: "my-edc" MY_EDC_TITLE: "EDC Connector" @@ -63,7 +64,8 @@ services: edc2: image: ${EDC_IMAGE} depends_on: - - postgresql2 + postgresql2: + condition: service_healthy environment: MY_EDC_PARTICIPANT_ID: "my-edc2" MY_EDC_TITLE: "EDC Connector 2" @@ -94,8 +96,56 @@ services: - '22004:11004' - '22005:5005' + test-backend: + image: ${TEST_BACKEND_IMAGE} + ports: + - '33001:11001' + + broker-ui: + image: ${EDC_UI_IMAGE} + ports: + - '44000:8080' + environment: + EDC_UI_ACTIVE_PROFILE: broker + EDC_UI_MANAGEMENT_API_URL: http://localhost:44002/backend/api/management + EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue + NGINX_ACCESS_LOG: off + + broker: + image: ${BROKER_IMAGE} + depends_on: + broker-postgresql: + condition: service_healthy + environment: + EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://edc:11003/api/dsp,http://edc2:11003/api/dsp" + EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" + + # Hide offline data offers after 1 minute in dev + EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" + + MY_EDC_FQDN: "broker" + EDC_API_AUTH_KEY: ApiKeyDefaultValue + + MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc + MY_EDC_JDBC_USER: edc + MY_EDC_JDBC_PASSWORD: edc + + # docker compose local dev environment overrides (don't use with non-dev images) + MY_EDC_PROTOCOL: "http://" + EDC_DSP_CALLBACK_ADDRESS: http://broker:44003/backend/api/dsp + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: + - '44001:11001' + - '44002:11002' + - '44003:11003' + - '44004:11004' + - '44005:5005' + postgresql: - image: docker.io/bitnami/postgresql:11 + image: docker.io/bitnami/postgresql:15 restart: always environment: POSTGRESQL_USERNAME: edc @@ -105,9 +155,14 @@ services: - '54321:5432' volumes: - 'postgresql:/bitnami/postgresql' + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U edc" ] + interval: 1s + timeout: 5s + retries: 10 postgresql2: - image: docker.io/bitnami/postgresql:11 + image: docker.io/bitnami/postgresql:15 restart: always environment: POSTGRESQL_USERNAME: edc @@ -117,14 +172,33 @@ services: - '54322:5432' volumes: - 'postgresql2:/bitnami/postgresql' + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U edc" ] + interval: 1s + timeout: 5s + retries: 10 - test-backend: - image: ${TEST_BACKEND_IMAGE} + broker-postgresql: + image: docker.io/bitnami/postgresql:15 + restart: always + environment: + POSTGRESQL_USERNAME: edc + POSTGRESQL_PASSWORD: edc + POSTGRESQL_DATABASE: edc ports: - - '33001:11001' + - '54323:5432' + volumes: + - 'broker-postgresql:/bitnami/postgresql' + healthcheck: + test: ["CMD-SHELL", "pg_isready -U edc"] + interval: 1s + timeout: 5s + retries: 10 volumes: postgresql: driver: local postgresql2: driver: local + broker-postgresql: + driver: local diff --git a/docs/deployment-guide/goals/broker-production/README.md b/docs/deployment-guide/goals/broker-production/README.md new file mode 100644 index 000000000..58d600ec7 --- /dev/null +++ b/docs/deployment-guide/goals/broker-production/README.md @@ -0,0 +1,130 @@ +# Broker Productive Deployment Guide + +## About this Guide + +This is a productive deployment guide for self-hosting a functional sovity Broker. + +## Deployment Units + +| Deployment Unit | Version / Details | +|----------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | +| Postgresql | 15 or compatible version | +| Broker Backend | broker-server-ce, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | +| Broker UI | edc-ui, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | + +### Configuration + +There is a [docker-compose.yaml](../../../../docker-compose.yaml) to try out the broker locally. +However, a productive release will require a few more configuration options, +so you should only use it to check if the released version is roughly working or if it's broken. + +#### Reverse Proxy Configuration + +- The broker is meant to be served via TLS/HTTPS. +- The broker is meant to be deployed with a reverse proxy merging the following ports: + - The UI's `8080` port. + - The Backend's `11002` port. + - The Backend's `11003` port. +- The mapping should look like this: + - `https://[MY_EDC_FQDN]/backend/api/dsp` -> `broker-backend:11003/backend/api/dsp` + - `https://[MY_EDC_FQDN]/backend/api/management` -> `broker-backend:11002/backend/api/management` + - All other requests -> `broker-ui:8080` + +#### Backend Configuration + +A productive configuration will require you to join a DAPS. + +For that you will need a SKI/AKI ClientID. Please refer +to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-extensions/tree/main/docs/getting-started#faq) +on how to generate one. + +The DAPS needs to contain the claim `referringConnector=broker` for the broker. +The expected value `broker` could be overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. + +```yaml +# Required: Fully Qualified Domain Name +MY_EDC_FQDN: "example.com" + +# Required: DB +MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc +MY_EDC_JDBC_USER: edc +MY_EDC_JDBC_PASSWORD: edc + +# Required: List of EDCs to fetch +EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/api/dsp,https://connector-b/api/dsp" + +# List of Data Space Names for special Connectors (default: '') +EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/api/dsp,OtherDataspace=https://some-other-connector/api/dsp" + +# Required: DAPS credentials +EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' +EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' +EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' +EDC_KEYSTORE: '_your keystore file_' # Needs to be available as file in the running container +EDC_KEYSTORE_PASSWORD: '_your keystore password_' +EDC_OAUTH_CERTIFICATE_ALIAS: 1 +EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 + +# Required: Management API Key +EDC_API_AUTH_KEY: "ApiKeyDefaultValue" + +# Required: Admin Api Key +EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey +``` + +All pre-configured config values for either the broker server or the underlying EDC can be found +in [launcher/.env.broker](../../../../launchers/.env.broker). + +#### UI Configuration + +```yaml +# Required: Profile +EDC_UI_ACTIVE_PROFILE: broker + +# Required: Management API URL +EDC_UI_MANAGEMENT_API_URL: https://my-broker.com/backend/api/management + +# Required: Management API Key +EDC_UI_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" +``` + +#### Adding Connectors at runtime + +Connectors can be dynamically added at runtime by using the following endpoint: + +```shell script +# Response should be 204 No Content +curl --request PUT \ + --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --header 'Content-Type: application/json' \ + --header 'x-api-key: ApiKeyDefaultValue' \ + --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' +``` + +#### Removing Connectors at runtime + +Connectors can be dynamically removed at runtime by using the following endpoint: + +```shell script +# Response should be 204 No Content +curl --request DELETE \ + --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ + --header 'Content-Type: application/json' \ + --header 'x-api-key: ApiKeyDefaultValue' \ + --data '["https://some-connector-to-be-removed/api/dsp", "https://some-other-connector-to-be-removed/api/dsp"]' +``` + +

      (back to top)

      + +## License + +Distributed under the Apache 2.0 License. See `LICENSE` for more information. + +

      (back to top)

      + +## Contact + +contact@sovity.de + +

      (back to top)

      diff --git a/docs/deployment-guide/goals/local-demo/README.md b/docs/deployment-guide/goals/local-demo/README.md index 4bd29ed37..f5c3e26cd 100644 --- a/docs/deployment-guide/goals/local-demo/README.md +++ b/docs/deployment-guide/goals/local-demo/README.md @@ -52,11 +52,15 @@ EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up ## Quick Start: Default Configuration -The default configuration launches two local EDC Connectors with the following credentials: - -| | First Connector | Second Connector | -|---------------------|---------------------------------------------------------------|:------------------------------------------------------------------------| -| Homepage | http://localhost:11000 | http://localhost:22000 | -| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | -| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | -| Connector Endpoint | http://edc:11003/api/dsp
      Requires Docker Compose Network | http://edc2:22003/api/dsp
      Requires Docker Compose Network | +The default configuration launches two local EDC Connectors and a Broker with the following credentials: + +| | First Connector | Second Connector | Broker | +|---------------------|---------------------------------------------------------------|:------------------------------------------------------------------------|------------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | http://localhost:44000 | +| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | http://localhost:44002/api/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://edc:11003/api/dsp
      Requires Docker Compose Network | http://edc2:22003/api/dsp
      Requires Docker Compose Network | http://broker:11003/api/dsp
      Requires Docker Compose Network | + +The Broker is configured to scan both connectors. + +

      (back to top)

      diff --git a/docs/deployment-guide/goals/production/4.2.0/README.md b/docs/deployment-guide/goals/production/4.2.0/README.md index 5ca9dcb49..92df18dc0 100644 --- a/docs/deployment-guide/goals/production/4.2.0/README.md +++ b/docs/deployment-guide/goals/production/4.2.0/README.md @@ -93,9 +93,9 @@ A sovity EDC CE or MDS EDC CE Backend deployment requires: - The following configuration properties > [!WARNING] -> Please be careful with overriding any of the ENV Vars set in our [launchers/.env](../../../../../launchers/.env). Our defaults -> will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as trailing -> slashes. +> Please be careful with overriding any of the ENV Vars set in our [launchers/.env.connector](../../../../../launchers/.env.connector). +> Our defaults will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as +> trailing slashes. ```yaml # Connector Host Name @@ -169,7 +169,7 @@ You can use a script (if you're on WSL or Linux) to generate the SKI, AKI and jk ### Can I run a connector locally and consume data from an online connector? No, locally run connectors cannot exchange data with online connectors. A connector must have a proper URL + -configuration and be accesible from the data provider via REST calls. +configuration and be accessible from the data provider via REST calls. ### (MDS Only) Can I disable the Broker- and/or ClearingHouse-Client-Extensions dynamically? diff --git a/docs/deployment-guide/goals/production/README.md b/docs/deployment-guide/goals/production/README.md index ca68cd0cb..3750324c0 100644 --- a/docs/deployment-guide/goals/production/README.md +++ b/docs/deployment-guide/goals/production/README.md @@ -1,5 +1,4 @@ -Productive Deployment Guide -======== +# Productive Deployment Guide > This is for our latest version. There is another guide for [4.2.0](4.2.0/README.md). @@ -100,7 +99,7 @@ EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD: https://[EDC_URL]/api/control/mana A sovity EDC CE or MDS EDC CE Backend deployment requires the following environment variables: > [!WARNING] -> Please be careful with overriding any of the ENV Vars set in our [launchers/.env](../../../../launchers/.env). Our +> Please be careful with overriding any of the ENV Vars set in our [launchers/.env.connector](../../../../launchers/.env.connector). Our > defaults > will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as trailing > slashes. diff --git a/docs/dev/changelog_updates.md b/docs/dev/changelog_updates.md index 528935c54..1d2df010b 100644 --- a/docs/dev/changelog_updates.md +++ b/docs/dev/changelog_updates.md @@ -12,7 +12,7 @@ For each release there will be a separate section especially with an "Overview" from a product perspective. Releases will especially contain a "Compatible Versions" section with the final docker -images and versions of our Connector Backend and Frontend. +images and versions of other software components that are connected by APIs. ## How to categorize a change @@ -27,7 +27,7 @@ Major changes include: - UX / Product overhauls. - Breaking changes in Connector-To-Connector communication. -- Breaking changes to other deployment units (our UI doesn't count). +- Breaking changes to the required deployment units (our UI doesn't count). - Breaking changes in our API Wrapper Use Case API. ### Minor Changes @@ -36,11 +36,12 @@ Minor changes include: - Any changes from a product perspective to our UI or API Wrapper UI API. - Additions to our API Wrapper Use Case API. +- New APIs with API contracts with other deployment units (our UI doesn't count). - New Product Documentation ### Patch Changes -Patch changes are basically everything else: +Patch changes are basically everything else, that does not add, change or remove any product or external API features. - Product Fixes, Bugfixes, Refactorings - Changes to existing Product Documentation diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts index 2806f4d97..a34c94429 100644 --- a/extensions/broker-server-api/api/build.gradle.kts +++ b/extensions/broker-server-api/api/build.gradle.kts @@ -1,38 +1,29 @@ -val sovityEdcGroup: String by project -val sovityEdcExtensionsVersion: String by project plugins { `java-library` `maven-publish` - id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.18" //./gradlew clean resolve - id("org.hidetake.swagger.generator") version "2.19.2" //./gradlew generateSwaggerUI - id("org.openapi.generator") version "7.0.1" //./gradlew openApiValidate && ./gradlew openApiGenerate + alias(libs.plugins.swagger.plugin) //./gradlew clean resolve + alias(libs.plugins.hidetake.swaggerGenerator) //./gradlew generateSwaggerUI + alias(libs.plugins.openapi.generator7) //./gradlew openApiValidate && ./gradlew openApiGenerate } dependencies { - annotationProcessor("org.projectlombok:lombok:1.18.30") - compileOnly("org.projectlombok:lombok:1.18.30") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) - api("${sovityEdcGroup}:wrapper-common-api:${sovityEdcExtensionsVersion}") + api(project(":extensions:wrapper:wrapper-common-api")) - api("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - api("jakarta.validation:jakarta.validation-api:3.0.2") - api("io.swagger.core.v3:swagger-annotations-jakarta:2.2.18") - api("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.18") - api("jakarta.servlet:jakarta.servlet-api:5.0.0") + api(libs.jakarta.rsApi) + api(libs.jakarta.validationApi) + api(libs.swagger.annotationsJakarta) + api(libs.swagger.jaxrs2Jakarta) + api(libs.jakarta.servletApi) - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("jakarta.validation:jakarta.validation-api:3.0.2") - implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("io.swagger.core.v3:swagger-annotations-jakarta:2.2.18") - implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.18") - implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") - implementation("jakarta.validation:jakarta.validation-api:3.0.2") - implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") - implementation("org.apache.commons:commons-lang3:3.13.0") + implementation(libs.apache.commonsLang) + implementation(libs.jakarta.validationApi) } -val openapiFileDir = "${project.buildDir}/swagger" +val openapiFileDir = project.layout.buildDirectory.get().asFile.resolve("swagger").path val openapiFileFilename = "broker-server.yaml" val openapiFile = "$openapiFileDir/$openapiFileFilename" @@ -77,8 +68,7 @@ tasks.withType { } } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index 845d3c6e0..9150baa0d 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -1,13 +1,8 @@ -val edcVersion: String by project -val edcGroup: String by project -val restAssured: String by project -val assertj: String by project - plugins { `java-library` `maven-publish` - id("org.openapi.generator") version "7.0.1" + alias(libs.plugins.openapi.generator7) } repositories { @@ -24,19 +19,19 @@ dependencies { } // Generated Client's Dependencies - implementation("io.swagger:swagger-annotations:1.6.12") - implementation("com.google.code.findbugs:jsr305:3.0.2") - implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") - implementation("com.google.code.gson:gson:2.10.1") - implementation("io.gsonfire:gson-fire:1.8.5") - implementation("org.openapitools:jackson-databind-nullable:0.2.6") - implementation("org.apache.commons:commons-lang3:3.13.0") - implementation("jakarta.annotation:jakarta.annotation-api:1.3.5") + implementation(libs.swagger.annotations) + implementation(libs.findbugs.jsr305) + implementation(libs.okhttp.okhttp) + implementation(libs.okhttp.loggingInterceptor) + implementation(libs.gson) + implementation(libs.gsonFire) + implementation(libs.openapi.jacksonDatabindNullable) + implementation(libs.apache.commonsLang) + implementation(libs.jakarta.annotationApi) // Lombok - compileOnly("org.projectlombok:lombok:1.18.30") - annotationProcessor("org.projectlombok:lombok:1.18.30") + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) } tasks.getByName("test") { @@ -44,19 +39,21 @@ tasks.getByName("test") { } // Extract the openapi file from the JAR -val openapiFile = "broker-server.yaml" -task("extractOpenapiYaml") { +val openapiFileName = "broker-server.yaml" +val targetLocation = project.buildDir.resolve("openapi") +val extractOpenapiYaml by tasks.registering(Copy::class) { dependsOn(openapiYaml) - into("${project.buildDir}") + into(targetLocation) from(zipTree(openapiYaml.singleFile)) { - include("broker-server.yaml") + include(openapiFileName) } } -tasks.getByName("openApiGenerate") { - dependsOn("extractOpenapiYaml") +val openApiGenerate = tasks.getByName("openApiGenerate") { + dependsOn(extractOpenapiYaml) generatorName.set("java") - configOptions.set(mutableMapOf( + configOptions.set( + mutableMapOf( "invokerPackage" to "de.sovity.edc.ext.brokerserver.client.gen", "apiPackage" to "de.sovity.edc.ext.brokerserver.client.gen.api", "modelPackage" to "de.sovity.edc.ext.brokerserver.client.gen.model", @@ -65,14 +62,15 @@ tasks.getByName("op "annotationLibrary" to "swagger1", "hideGenerationTimestamp" to "true", "useRuntimeException" to "true", - )) + ) + ) - inputSpec.set("${project.buildDir}/${openapiFile}") + inputSpec.set(targetLocation.resolve(openapiFileName).path) outputDir.set("${project.buildDir}/generated/client-project") } -task("postprocessGeneratedClient") { - dependsOn("openApiGenerate") +val postprocessGeneratedClient by tasks.registering(Copy::class) { + dependsOn(openApiGenerate) from("${project.buildDir}/generated/client-project/src/main/java") // @lombok.Builder clashes with the following generated model file. @@ -95,14 +93,25 @@ checkstyle { tasks.getByName("compileJava") { - dependsOn("postprocessGeneratedClient") + dependsOn(postprocessGeneratedClient) +} + +val sourcesJar = tasks.getByName("sourcesJar") { + dependsOn(postprocessGeneratedClient) +} + +val javadocJar = tasks.getByName("javadocJar") { + dependsOn(postprocessGeneratedClient) +} + +artifacts { + add("archives", sourcesJar) + add("archives", javadocJar) } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 - withSourcesJar() - withJavadocJar() } tasks.withType { @@ -111,8 +120,7 @@ tasks.withType { fullOptions.addStringOption("Xdoclint:none", "-quiet") } -val sovityBrokerServerGroup: String by project -group = sovityBrokerServerGroup +group = libs.versions.sovityBrokerServerGroup.get() publishing { publications { diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts index 6956ef074..9320b4c62 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts @@ -5,7 +5,7 @@ import org.testcontainers.containers.PostgreSQLContainer val jooqDbType = "org.jooq.meta.postgres.PostgresDatabase" val jdbcDriver = "org.postgresql.Driver" -val postgresContainer = "postgres:11-alpine" +val postgresContainer = libs.versions.postgresDbImage.get() val migrationsDir = "src/main/resources/db/migration" val testDataDir = "src/main/resources/db/testdata" @@ -15,44 +15,39 @@ val jooqTargetSourceRoot = "build/generated/jooq" val jooqTargetDir = jooqTargetSourceRoot + "/" + jooqTargetPackage.replace(".", "/") val flywayMigration = configurations.create("flywayMigration") -val edcVersion: String by project -val edcGroup: String by project -val flywayVersion: String by project -val postgresVersion: String by project - buildscript { dependencies { - classpath("org.testcontainers:postgresql:1.19.1") + classpath(libs.testcontainers.postgresql) } } plugins { - id("org.flywaydb.flyway") version "9.21.1" - id("nu.studer.jooq") version "7.1.1" + alias(libs.plugins.flyway) + alias(libs.plugins.jooq) `java-library` `maven-publish` } dependencies { - api("org.jooq:jooq:3.18.7") - api("com.github.t9t.jooq:jooq-postgresql-json:4.0.0") + api(libs.jooq.jooq) + api(libs.t9tJooq.jooqPostgresqlJson) - jooqGenerator("org.postgresql:postgresql:42.7.2") - flywayMigration("org.postgresql:postgresql:42.7.2") - implementation("com.zaxxer:HikariCP:5.0.1") + jooqGenerator(libs.postgres) + flywayMigration(libs.postgres) + implementation(libs.hikari) - annotationProcessor("org.projectlombok:lombok:1.18.30") - compileOnly("org.projectlombok:lombok:1.18.30") - implementation("org.apache.commons:commons-lang3:3.13.0") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + implementation(libs.apache.commonsLang) - implementation("${edcGroup}:core-spi:${edcVersion}") + implementation(libs.edc.coreSpi) // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) - implementation("org.postgresql:postgresql:${postgresVersion}") + implementation(libs.postgres) - api("org.flywaydb:flyway-core:${flywayVersion}") + api(libs.flyway.core) - testImplementation("${edcGroup}:junit:${edcVersion}") + testImplementation(libs.edc.junit) } sourceSets { @@ -185,6 +180,7 @@ tasks.withType { } } +group = libs.versions.sovityBrokerServerGroup.get() publishing { publications { diff --git a/extensions/broker-server/README.md b/extensions/broker-server/README.md index 2da3e724f..103af330b 100644 --- a/extensions/broker-server/README.md +++ b/extensions/broker-server/README.md @@ -16,7 +16,7 @@ ## About this Extension -Implementation of an IDS Broker Server as an EDC Extension. +Implementation of an EDC Broker backend as an EDC Extension. This extension does multiple things: diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index 444e5071a..a304e1274 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -1,67 +1,57 @@ plugins { `java-library` - id("org.gradle.test-retry") version "1.5.7" + alias(libs.plugins.retry) } -val edcVersion: String by project -val edcGroup: String by project -val jupiterVersion: String by project -val mockitoVersion: String by project -val assertj: String by project -val okHttpVersion: String by project -val restAssured: String by project -val testcontainersVersion: String by project -val sovityEdcGroup: String by project -val sovityEdcExtensionGroup: String by project -val sovityEdcExtensionsVersion: String by project - configurations.all { resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) } dependencies { - annotationProcessor("org.projectlombok:lombok:1.18.30") - compileOnly("org.projectlombok:lombok:1.18.30") - implementation("org.apache.commons:commons-lang3:3.13.0") + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + implementation(libs.apache.commonsLang) - api("${sovityEdcGroup}:catalog-parser:${sovityEdcExtensionsVersion}") { isChanging = true } - api("${sovityEdcGroup}:json-and-jsonld-utils:${sovityEdcExtensionsVersion}") { isChanging = true } - api("${sovityEdcGroup}:wrapper-common-mappers:${sovityEdcExtensionsVersion}") { isChanging = true } + api(project(":utils:catalog-parser")) + api(project(":utils:json-and-jsonld-utils")) + api(project(":extensions:wrapper:wrapper-common-mappers")) - implementation("${edcGroup}:control-plane-spi:${edcVersion}") - implementation("${edcGroup}:management-api-configuration:${edcVersion}") + implementation(libs.edc.controlPlaneSpi) + implementation(libs.edc.managementApiConfiguration) api(project(":extensions:broker-server-postgres-flyway-jooq")) implementation(project(":extensions:broker-server-api:api")) + implementation(project(":utils:versions")) - implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") + implementation(libs.okhttp.okhttp) - testAnnotationProcessor("org.projectlombok:lombok:1.18.30") - testCompileOnly("org.projectlombok:lombok:1.18.30") - testImplementation("${sovityEdcGroup}:client:${sovityEdcExtensionsVersion}") { isChanging = true } - testImplementation("${sovityEdcExtensionGroup}:sovity-edc-extensions-package:${sovityEdcExtensionsVersion}") { isChanging = true } - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.mockito:mockito-inline:${mockitoVersion}") - testImplementation("${edcGroup}:control-plane-core:${edcVersion}") - testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") - testImplementation("${edcGroup}:junit:${edcVersion}") - testImplementation("${edcGroup}:http:${edcVersion}") - testImplementation("${edcGroup}:iam-mock:${edcVersion}") - testImplementation("${edcGroup}:dsp:${edcVersion}") - testImplementation("${edcGroup}:json-ld:${edcVersion}") - testImplementation("${edcGroup}:monitor-jdk-logger:${edcVersion}") - testImplementation("${edcGroup}:configuration-filesystem:${edcVersion}") + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + testImplementation(project(":extensions:wrapper:clients:java-client")) + testImplementation(project(":extensions:sovity-edc-extensions-package")) + testImplementation(libs.assertj.core) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.edc.controlPlaneCore) + testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.edc.junit) + testImplementation(libs.edc.http) + testImplementation(libs.edc.iamMock) + testImplementation(libs.edc.dsp) + testImplementation(libs.edc.jsonLd) + testImplementation(libs.edc.monitorJdkLogger) + testImplementation(libs.edc.configurationFilesystem) testImplementation(project(":extensions:broker-server-api:client")) - testImplementation("io.rest-assured:rest-assured:${restAssured}") - testImplementation("org.testcontainers:testcontainers:${testcontainersVersion}") - testImplementation("org.testcontainers:junit-jupiter:${testcontainersVersion}") - testImplementation("org.testcontainers:postgresql:${testcontainersVersion}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") - testImplementation("org.skyscreamer:jsonassert:1.5.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.testcontainers.junitJupiter) + testImplementation(libs.testcontainers.postgresql) + testImplementation(libs.junit.api) + testImplementation(libs.jsonAssert) + testRuntimeOnly(libs.junit.engine) - implementation("org.quartz-scheduler:quartz:2.3.2") + implementation(libs.quartz.quartz) } tasks.getByName("test") { @@ -76,6 +66,8 @@ tasks.getByName("test") { tasks.register("prepareKotlinBuildScriptModel") {} +group = libs.versions.sovityBrokerServerGroup.get() + publishing { publications { create(project.name) { diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java index a46fe2f0c..cc272fcb8 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java @@ -21,15 +21,11 @@ public class TestDatabaseFactory { /** - * Returns a JUnit 5 Extension that either connects to a test database or launches a testcontainer. + * Returns a JUnit 5 Extension that launches a testcontainer. * * @return {@link TestDatabase} */ public static TestDatabase getTestDatabase() { - if (TestDatabaseViaEnv.isSkipTestcontainers()) { - return new TestDatabaseViaEnv(); - } - return new TestDatabaseViaTestcontainers(); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java index 73134808f..fd3d1caf0 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java @@ -14,11 +14,12 @@ package de.sovity.edc.ext.brokerserver.db; +import de.sovity.edc.utils.versions.GradleVersions; import org.junit.jupiter.api.extension.ExtensionContext; import org.testcontainers.containers.PostgreSQLContainer; public class TestDatabaseViaTestcontainers implements TestDatabase { - private PostgreSQLContainer container = new PostgreSQLContainer<>("postgres:15-alpine") + private PostgreSQLContainer container = new PostgreSQLContainer<>(GradleVersions.POSTGRES_IMAGE_TAG) .withUsername("edc") .withPassword("edc"); diff --git a/extensions/edc-ui-config/build.gradle.kts b/extensions/edc-ui-config/build.gradle.kts index 56d0c920f..1f193451f 100644 --- a/extensions/edc-ui-config/build.gradle.kts +++ b/extensions/edc-ui-config/build.gradle.kts @@ -1,9 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val restAssured: String by project -val jettyVersion: String by project -val jettyGroup: String by project -val mockitoVersion: String by project plugins { `java-library` @@ -40,8 +34,7 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/last-commit-info/build.gradle.kts b/extensions/last-commit-info/build.gradle.kts index 2d4f01a0a..eac954d07 100644 --- a/extensions/last-commit-info/build.gradle.kts +++ b/extensions/last-commit-info/build.gradle.kts @@ -1,10 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val restAssured: String by project -val mockitoVersion: String by project -val lombokVersion: String by project -val jettyVersion: String by project -val jettyGroup: String by project plugins { `java-library` @@ -47,8 +40,7 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/policy-always-true/build.gradle.kts b/extensions/policy-always-true/build.gradle.kts index 9122cf0df..dd96f4f1a 100644 --- a/extensions/policy-always-true/build.gradle.kts +++ b/extensions/policy-always-true/build.gradle.kts @@ -1,6 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val mockitoVersion: String by project plugins { `java-library` @@ -21,8 +18,7 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/policy-referring-connector/build.gradle.kts b/extensions/policy-referring-connector/build.gradle.kts index 04287b42f..c7fa700f5 100644 --- a/extensions/policy-referring-connector/build.gradle.kts +++ b/extensions/policy-referring-connector/build.gradle.kts @@ -1,7 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val mockitoVersion: String by project -val jupiterVersion: String by project plugins { `java-library` @@ -23,8 +19,7 @@ tasks.withType { useJUnitPlatform() } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/policy-time-interval/build.gradle.kts b/extensions/policy-time-interval/build.gradle.kts index e36914450..66b2183c3 100644 --- a/extensions/policy-time-interval/build.gradle.kts +++ b/extensions/policy-time-interval/build.gradle.kts @@ -1,5 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project plugins { `java-library` @@ -12,8 +10,7 @@ dependencies { testImplementation(libs.edc.junit) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/postgres-flyway/README.md b/extensions/postgres-flyway/README.md index a62f29b4e..8ee2998ba 100644 --- a/extensions/postgres-flyway/README.md +++ b/extensions/postgres-flyway/README.md @@ -30,7 +30,7 @@ The extension includes the edc-stores for the following edc-types: - policy - transferprocess -Futhermore, the `ConnectionsPool`, `transaction`-Extensions and the JDBC-Driver for the +Furthermore, the `ConnectionsPool`, `transaction`-Extensions and the JDBC-Driver for the PostgreSQL-Database are provided. The tables are prepared using Flyway, which executes the .sql scripts included in diff --git a/extensions/postgres-flyway/build.gradle.kts b/extensions/postgres-flyway/build.gradle.kts index 42089ae9b..e4f6f0ba6 100644 --- a/extensions/postgres-flyway/build.gradle.kts +++ b/extensions/postgres-flyway/build.gradle.kts @@ -1,10 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val tractusVersion: String by project -val tractusGroup: String by project -val flywayVersion: String by project -val postgresVersion: String by project -val lombokVersion: String by project plugins { `java-library` @@ -30,8 +23,7 @@ dependencies { testImplementation(libs.edc.junit) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/sovity-edc-extensions-package/build.gradle.kts b/extensions/sovity-edc-extensions-package/build.gradle.kts index 69af1e1ee..10a522670 100644 --- a/extensions/sovity-edc-extensions-package/build.gradle.kts +++ b/extensions/sovity-edc-extensions-package/build.gradle.kts @@ -1,6 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val restAssured: String by project plugins { `java-library` @@ -19,8 +16,7 @@ dependencies { api(project(":extensions:wrapper:wrapper")) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/test-backend-controller/build.gradle.kts b/extensions/test-backend-controller/build.gradle.kts index 41e9dee8a..9e5364de4 100644 --- a/extensions/test-backend-controller/build.gradle.kts +++ b/extensions/test-backend-controller/build.gradle.kts @@ -1,6 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project - plugins { `java-library` } @@ -11,8 +8,7 @@ dependencies { api(libs.edc.http) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/transfer-process-status-checker/build.gradle.kts b/extensions/transfer-process-status-checker/build.gradle.kts index 5bc93ae54..e420cbdaf 100644 --- a/extensions/transfer-process-status-checker/build.gradle.kts +++ b/extensions/transfer-process-status-checker/build.gradle.kts @@ -1,6 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project - plugins { `java-library` `maven-publish` @@ -11,8 +8,7 @@ dependencies { testImplementation(libs.edc.junit) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/wrapper/clients/java-client/build.gradle.kts b/extensions/wrapper/clients/java-client/build.gradle.kts index a6783b3d9..65a99230f 100644 --- a/extensions/wrapper/clients/java-client/build.gradle.kts +++ b/extensions/wrapper/clients/java-client/build.gradle.kts @@ -1,17 +1,8 @@ -val edcVersion: String by project -val edcGroup: String by project -val restAssured: String by project -val assertj: String by project -val mockitoVersion: String by project -val lombokVersion: String by project -val jettyVersion: String by project -val jettyGroup: String by project - plugins { `java-library` `maven-publish` - alias(libs.plugins.openapi.generator) + alias(libs.plugins.openapi.generator6) } repositories { @@ -36,7 +27,7 @@ dependencies { implementation(libs.gsonFire) implementation(libs.openapi.jacksonDatabindNullable) implementation(libs.apache.commonsLang) - implementation(libs.jakarta.annotation) + implementation(libs.jakarta.annotationApi) // Lombok compileOnly(libs.lombok) @@ -121,8 +112,7 @@ tasks.withType { fullOptions.addStringOption("Xdoclint:none", "-quiet") } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() publishing { publications { diff --git a/extensions/wrapper/wrapper-api/build.gradle.kts b/extensions/wrapper/wrapper-api/build.gradle.kts index 5ddf07630..74003ce23 100644 --- a/extensions/wrapper/wrapper-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-api/build.gradle.kts @@ -1,18 +1,10 @@ -val edcVersion: String by project -val edcGroup: String by project -val restAssured: String by project -val assertj: String by project -val mockitoVersion: String by project -val lombokVersion: String by project -val jettyVersion: String by project -val jettyGroup: String by project plugins { `java-library` `maven-publish` alias(libs.plugins.swagger.plugin) //./gradlew clean resolve alias(libs.plugins.hidetake.swaggerGenerator) //./gradlew generateSwaggerUI - alias(libs.plugins.openapi.generator) //./gradlew openApiValidate && ./gradlew openApiGenerate + alias(libs.plugins.openapi.generator6) //./gradlew openApiValidate && ./gradlew openApiGenerate } dependencies { @@ -27,7 +19,7 @@ dependencies { implementation(libs.jakarta.rsApi) implementation(libs.swagger.annotationsJakarta) implementation(libs.swagger.jaxrs2Jakarta) - implementation(libs.jakarta.servlet) + implementation(libs.jakarta.servletApi) implementation(libs.jakarta.validationApi) implementation(libs.jakarta.rsApi) implementation(libs.apache.commonsLang) @@ -86,8 +78,7 @@ tasks.withType { } } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java index 6944a3d65..205beb02b 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java @@ -20,7 +20,7 @@ /** * Contract Definition Criterion * - * @see org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl + * @see
      org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl
      */ @Getter @RequiredArgsConstructor diff --git a/extensions/wrapper/wrapper-common-api/build.gradle.kts b/extensions/wrapper/wrapper-common-api/build.gradle.kts index 083561a82..415a7545b 100644 --- a/extensions/wrapper/wrapper-common-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-api/build.gradle.kts @@ -1,4 +1,3 @@ -val lombokVersion: String by project plugins { `java-library` @@ -13,13 +12,12 @@ dependencies { api(libs.jakarta.validationApi) api(libs.swagger.annotationsJakarta) api(libs.swagger.jaxrs2Jakarta) - api(libs.jakarta.servlet) + api(libs.jakarta.servletApi) implementation(libs.apache.commonsLang) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() publishing { publications { diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts index 71fd5b3f8..aa42db904 100644 --- a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -1,10 +1,3 @@ -val lombokVersion: String by project - -val assertj: String by project -val edcGroup: String by project -val edcVersion: String by project -val jsonUnit: String by project -val mockitoVersion: String by project plugins { `java-library` @@ -38,8 +31,7 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() publishing { publications { diff --git a/extensions/wrapper/wrapper-ee-api/build.gradle.kts b/extensions/wrapper/wrapper-ee-api/build.gradle.kts index bc2cae006..46a74b767 100644 --- a/extensions/wrapper/wrapper-ee-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-ee-api/build.gradle.kts @@ -1,4 +1,3 @@ -val lombokVersion: String by project plugins { `java-library` @@ -15,14 +14,13 @@ dependencies { api(libs.jakarta.validationApi) api(libs.swagger.annotationsJakarta) api(libs.swagger.jaxrs2Jakarta) - api(libs.jakarta.servlet) + api(libs.jakarta.servletApi) implementation(libs.apache.commonsLang) implementation(libs.jersey.mediaMultipart) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() publishing { publications { diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts index c0ebb5be7..840ce4349 100644 --- a/extensions/wrapper/wrapper/build.gradle.kts +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -1,12 +1,3 @@ -val assertj: String by project -val edcVersion: String by project -val edcGroup: String by project -val jettyGroup: String by project -val jettyVersion: String by project -val jsonUnit: String by project -val lombokVersion: String by project -val mockitoVersion: String by project -val restAssured: String by project plugins { `java-library` @@ -70,8 +61,7 @@ tasks.withType { maxParallelForks = 1 } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java index 23d5d2e1c..f95e4b501 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_negotiations/ContractNegotiationStateService.java @@ -43,7 +43,7 @@ public ContractNegotiationState buildContractNegotiationState(int code) { /** * Which Transfer Process do we want to show as 'running' in our UI? * - * @param code {@link ContractNegotiation#getState()}, see {@link ContractNegotiationState#code()} + * @param code {@link ContractNegotiation#getState()}, see {@code ContractNegotiationState#code} * @return if running */ public boolean isRunning(int code) { diff --git a/gradle.properties b/gradle.properties index c367b1c63..c8b6c834e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,26 +1,4 @@ sovityEdcExtensionsVersion=0.0.1-SNAPSHOT -sovityEdcExtensionGroup=de.sovity.edc.ext -sovityEdcGroup=de.sovity.edc -edcGroup=org.eclipse.edc -edcVersion=0.2.1.2 -tractusGroup=org.eclipse.tractusx.edc -tractusVersion=0.5.3 -assertj=3.23.1 -jsonUnit=3.2.7 -jupiterVersion=5.8.2 -mockitoVersion=4.8.0 -okHttpVersion=4.10.0 -httpMockServerVersion=5.15.0 -jsonVersion=20230618 -restAssured=4.5.0 -flywayVersion=9.0.1 -postgresVersion=42.4.0 -testcontainersVersion=1.17.6 -lombokVersion=1.18.28 -awaitilityVersion=4.2.0 -jettyGroup=org.eclipse.jetty -jettyVersion=11.0.15 -jakartaJsonVersion=2.0.1 org.gradle.jvmargs=-Xmx1024m org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca6376cf0..31233c92f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,14 @@ [versions] +# groups +edcGroup="org.eclipse.edc" +sovityBrokerServerGroup = "de.sovity.broker" +sovityEdcExtensionGroup = "de.sovity.edc.ext" +sovityEdcGroup = "de.sovity.edc" + +# images +postgresDbImage = "postgres:15-alpine" + +# versions assertj = "3.23.1" awaitility = "4.2.0" commonsCompress = "1.26.1" @@ -9,39 +19,50 @@ edc = "0.2.1.2" findbugs = "3.0.2" flexmark = "0.64.8" flyway = "9.0.1" +flywayPlugin = "9.21.1" gson = "2.10.1" gsonFire = "1.8.5" guava = "33.1.0-jre" hidetakeSwagger = "2.19.2" +hikari = "5.0.1" jakartaAnnotation = "1.3.5" jakartaJson = "2.0.1" jakartaRs = "3.1.0" -jakartaServlet = "5.0.0" +jakartaServletApi = "5.0.0" jakartaValidation = "3.0.2" java = "17" +javapoet = "1.13.0" jersey = "3.1.3" jetty = "11.0.15" +jooq = "3.18.7" +jooqPostgresqlJson = "4.0.0" +jooqPlugin = "7.1.1" +json = "20220924" +jsonAssert = "1.5.1" jsonUnit = "3.2.7" junit = "5.10.0" loggingHouse = "0.2.10" -lombok = "1.18.28" +lombok = "1.18.30" mockito = "4.8.0" mockserver = "5.15.0" -okhttp = "4.11.0" +okhttp = "4.12.0" okio = "3.9.0" -openapiGenerator = "6.6.0" +openapiGenerator6 = "6.6.0" +openapiGenerator7 = "7.0.1" openapiJackson = "0.2.6" -postgres = "42.4.0" +postgres = "42.7.2" quarkus = "2.16.6.Final" +quartz = "2.3.2" restAssured = "4.5.0" retry = "1.5.7" shadow = "7.1.2" -swagger = "1.6.11" -swaggerCore = "2.2.15" -testcontainers = "1.17.6" +swagger = "1.6.12" +swaggerCore = "2.2.18" +testcontainers = "1.19.1" titaniumLd = "1.3.2" tractus = "0.5.3" + [libraries] apache-commonsCollections = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections" } @@ -61,6 +82,7 @@ edc-authSpi = { module = "org.eclipse.edc:auth-spi", version.ref = "edc" } edc-authTokenbased = { module = "org.eclipse.edc:auth-tokenbased", version.ref = "edc" } edc-boot = { module = "org.eclipse.edc:boot", version.ref = "edc" } edc-configurationFilesystem = { module = "org.eclipse.edc:configuration-filesystem", version.ref = "edc" } +edc-connectorCore = { module = "org.eclipse.edc:connector-core", version.ref = "edc" } edc-contractDefinitionApi = { module = "org.eclipse.edc:contract-definition-api", version.ref = "edc" } edc-contractSpi = { module = "org.eclipse.edc:contract-spi", version.ref = "edc" } edc-controlPlaneAggregateServices = { module = "org.eclipse.edc:control-plane-aggregate-services", version.ref = "edc" } @@ -110,10 +132,12 @@ gsonFire = { module = "io.gsonfire:gson-fire", version.ref = "gsonFire" } guava = { module = "com.google.guava:guava", version.ref = "guava" } -jakarta-annotation = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "jakartaAnnotation" } +hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } + +jakarta-annotationApi = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "jakartaAnnotation" } jakarta-json = { module = "org.glassfish:jakarta.json", version.ref = "jakartaJson" } jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "jakartaRs" } -jakarta-servlet = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakartaServlet" } +jakarta-servletApi = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakartaServletApi" } jakarta-validationApi = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakartaValidation" } jersey-mediaMultipart = { module = "org.glassfish.jersey.media:jersey-media-multipart", version.ref = "jersey" } @@ -125,6 +149,10 @@ jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty jetty-util = { module = "org.eclipse.jetty:jetty-util", version.ref = "jetty" } jetty-webapp = { module = "org.eclipse.jetty:jetty-webapp", version.ref = "jetty" } +jooq-jooq = { module = "org.jooq:jooq", version.ref = "jooq" } + +jsonAssert = { module = "org.skyscreamer:jsonassert", version.ref = "jsonAssert" } + jsonUnit-assertj = { module = "net.javacrumbs.json-unit:json-unit-assertj", version.ref = "jsonUnit" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } @@ -152,12 +180,18 @@ postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } quarkus-bom = { module = "io.quarkus.platform:quarkus-bom", version.ref = "quarkus" } +quartz-quartz = { module = "org.quartz-scheduler:quartz", version.ref = "quartz" } + restAssured-restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } +squareup-javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" } + swagger-annotations = { module = "io.swagger:swagger-annotations", version.ref = "swagger" } swagger-annotationsJakarta = { module = "io.swagger.core.v3:swagger-annotations-jakarta", version.ref = "swaggerCore" } swagger-jaxrs2Jakarta = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version.ref = "swaggerCore" } +t9tJooq-jooqPostgresqlJson = { module = "com.github.t9t.jooq:jooq-postgresql-json", version.ref = "jooqPostgresqlJson" } + testcontainers-testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } testcontainers-junitJupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } @@ -176,8 +210,11 @@ jetty-cve2023 = [ [plugins] hidetake-swaggerGenerator = { id = "org.hidetake.swagger.generator", version.ref = "hidetakeSwagger" } -openapi-generator = { id = "org.openapi.generator", version.ref = "openapiGenerator" } +openapi-generator6 = { id = "org.openapi.generator", version.ref = "openapiGenerator6" } +openapi-generator7 = { id = "org.openapi.generator", version.ref = "openapiGenerator7" } quarkus = { id = "io.quarkus", version.ref = "quarkus" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } swagger-plugin = { id = "io.swagger.core.v3.swagger-gradle-plugin", version.ref = "swaggerCore" } retry = { id = "org.gradle.test-retry", version.ref = "retry" } +jooq = { id = "nu.studer.jooq", version.ref = "jooqPlugin" } +flyway = { id = "org.flywaydb.flyway", version.ref = "flywayPlugin" } diff --git a/connector/.env b/launchers/.env.broker similarity index 98% rename from connector/.env rename to launchers/.env.broker index ca37ee84c..cc006d7cd 100644 --- a/connector/.env +++ b/launchers/.env.broker @@ -103,4 +103,4 @@ EDC_AGENT_IDENTITY_KEY=referringConnector # This file could contain an entry replacing the EDC_KEYSTORE ENV var, # but for some reason it is required, and EDC won't start up if it isn't configured. # It will be created in the Dockerfile -EDC_VAULT=/emtpy-properties-file.properties +EDC_VAULT=/app/empty-properties-file.properties diff --git a/launchers/.env b/launchers/.env.connector similarity index 98% rename from launchers/.env rename to launchers/.env.connector index 19a9d73b5..d58c1267a 100644 --- a/launchers/.env +++ b/launchers/.env.connector @@ -94,4 +94,4 @@ EDC_AGENT_IDENTITY_KEY=referringConnector # This file could contain an entry replacing the EDC_KEYSTORE ENV var # but for some reason it is required, and EDC won't start up if it isn't configured # it is created in the Dockerfile -EDC_VAULT=/app/emtpy-properties-file.properties +EDC_VAULT=/app/empty-properties-file.properties diff --git a/launchers/Dockerfile b/launchers/Dockerfile index a4dbda971..b1e492b9b 100644 --- a/launchers/Dockerfile +++ b/launchers/Dockerfile @@ -10,6 +10,7 @@ USER edc:edc # Which app.jar to include ARG CONNECTOR_NAME="sovity-ce" +ARG CONNECTOR_TYPE="connector" # For last-commit-info extension ARG EDC_LAST_COMMIT_INFO_ARG="The docker container was built outside of github actions and you didn't provide the build arg EDC_LAST_COMMIT_INFO_ARG, so there's no last commit info." @@ -19,8 +20,9 @@ WORKDIR /app COPY ./launchers/connectors/$CONNECTOR_NAME/build/libs/app.jar /app COPY ./launchers/logging.properties /app COPY ./launchers/logging.dev.properties /app -COPY ./launchers/.env /app/.env -RUN touch /app/emtpy-properties-file.properties +COPY ./launchers/.env.$CONNECTOR_TYPE /app/.env + +RUN touch /app/empty-properties-file.properties # Replaces var statements so when they are sourced as bash they don't overwrite existing env vars RUN sed -ri 's/^\s*(\S+)=(.*)$/\1=${\1:-"\2"}/' .env @@ -34,4 +36,4 @@ ENTRYPOINT ["/app/entrypoint.sh"] CMD ["start"] # health status is determined by the availability of the /health endpoint -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:11001/api/check/health +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11001/backend/api/check/health diff --git a/launchers/README.md b/launchers/README.md index cb6363027..02d235771 100644 --- a/launchers/README.md +++ b/launchers/README.md @@ -35,12 +35,141 @@ functionalities for self-hosting purposes. Our sovity Community Edition EDC is built as several docker image variants in different configurations. -| Docker Image | Type | Purpose | Features | -|----------------------------------------------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [edc-dev](https://github.com/sovity/edc-extensions/pkgs/container/edc-dev) | Devevelopment |
      • Local manual testing
      • Local demos
      |
      • Control- and Data-Plane
      • sovity Community Edition EDC Extensions
      • Management API Auth via API Keys
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | -| [edc-ce](https://github.com/sovity/edc-extensions/pkgs/container/edc-ce) | sovity Community Edition |
      • Self-Deploy a productive sovity EDC
      |
      • Control- and Data-Plane
      • sovity Community Edition EDC Extensions
      • Management API Auth via API Keys
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | -| [edc-ce-mds](https://github.com/sovity/edc-extensions/pkgs/container/edc-ce-mds) | MDS Community Edition |
      • Self-Deploy a productive MDS EDC
      |
      • Control- and Data-Plane
      • sovity Community Edition EDC Extensions
      • Management API Auth via API Keys
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      • Broker Extension
      • Clearing House Extension
      | -| edc-ee | Commercial |
      • Productive use
      • Professional users
      • Our Connector-as-a-Service (CaaS) customers
      • [Request Demo](mailto:contact@sovity.de)
      |
      • Managed Control- and Data Planes, individually scalable
      • Hosted on highly performant infrastructure
      • Management API Auth via Service Accounts
      • Managed User Auth via standalone IAM (SSO)
      • Automatic Dataspace Roll-In, for example to Data Spaces like Catena-X or Mobility Data Space
      • Managed DAPS Authentication
      • Support & Tutorials
      • Automatic updates to newest version and new features
      • Off-the-shelf extensions for use cases available
      • EDC available within minutes
      • Can be combined with Data Space as a Service (DSaaS)
      | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Docker ImageTypePurposeFeatures
      + edc-dev + Development +
        +
      • Local manual testing
      • +
      • Local demos
      • +
      +
      +
        +
      • Control- and Data-Plane
      • +
      • sovity Community Edition EDC Extensions
      • +
      • Management API Auth via API Keys
      • +
      • PostgreSQL Persistence & Flyway
      • +
      • Mock IAM
      • +
      +
      + edc-ce + sovity Community Edition +
        +
      • Self-Deploy a productive sovity EDC
      • +
      +
      +
        +
      • Control- and Data-Plane
      • +
      • sovity Community Edition EDC Extensions
      • +
      • Management API Auth via API Keys
      • +
      • PostgreSQL Persistence & Flyway
      • +
      • DAPS Authentication
      • +
      +
      + edc-ce-mds + MDS Community Edition +
        +
      • Self-Deploy a productive MDS EDC
      • +
      +
      +
        +
      • Control- and Data-Plane
      • +
      • sovity Community Edition EDC Extensions
      • +
      • Management API Auth via API Keys
      • +
      • PostgreSQL Persistence & Flyway
      • +
      • DAPS Authentication
      • +
      • Broker Extension
      • +
      • Clearing House Extension
      • +
      +
      edc-eeCommercial +
        +
      • Productive use
      • +
      • Professional users
      • +
      • Our Connector-as-a-Service (CaaS) customers
      • +
      • Request Demo +
      +
      +
        +
      • Managed Control- and Data Planes, individually scalable
      • +
      • Hosted on highly performant infrastructure
      • +
      • Management API Auth via Service Accounts
      • +
      • Managed User Auth via standalone IAM (SSO)
      • +
      • Automatic Dataspace Roll-In, for example to Data Spaces like Catena-X or Mobility Data Space
      • +
      • Managed DAPS Authentication
      • +
      • Support & Tutorials
      • +
      • Automatic updates to newest version and new features
      • +
      • Off-the-shelf extensions for use cases available
      • +
      • EDC available within minutes
      • +
      • Can be combined with Data Space as a Service (DSaaS)
      • +
      +
      + broker-dev + Development +
        +
      • Local Demo via our + docker-compose.yaml +
      • +
      • E2E Testing
      • +
      +
      +
        +
      • Broker Server Extension(s)
      • +
      • PostgreSQL Persistence & Flyway
      • +
      • Mock IAM
      • +
      +
      broker-ceCommunity Edition +
        +
      • Productive Deployment
      • +
      +
      +
        +
      • Broker Server Extension(s)
      • +
      • PostgreSQL Persistence & Flyway
      • +
      • DAPS Authentication
      • +
      +
      ## Image Tags diff --git a/launchers/common/auth-daps/build.gradle.kts b/launchers/common/auth-daps/build.gradle.kts index 18e774404..8781e7e2a 100644 --- a/launchers/common/auth-daps/build.gradle.kts +++ b/launchers/common/auth-daps/build.gradle.kts @@ -2,14 +2,10 @@ plugins { `java-library` } -val edcVersion: String by project -val edcGroup: String by project - dependencies { // OAuth2 IAM api(libs.edc.oauth2Core) api(libs.edc.vaultFilesystem) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/common/auth-mock/build.gradle.kts b/launchers/common/auth-mock/build.gradle.kts index bf7d74f56..84a2fd9b7 100644 --- a/launchers/common/auth-mock/build.gradle.kts +++ b/launchers/common/auth-mock/build.gradle.kts @@ -2,13 +2,9 @@ plugins { `java-library` } -val edcVersion: String by project -val edcGroup: String by project - dependencies { // Mock IAM api(libs.edc.iamMock) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/common/base-mds/build.gradle.kts b/launchers/common/base-mds/build.gradle.kts index 313ed948b..0faceaa8d 100644 --- a/launchers/common/base-mds/build.gradle.kts +++ b/launchers/common/base-mds/build.gradle.kts @@ -6,5 +6,4 @@ dependencies { implementation(libs.loggingHouse.client) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/common/base/build.gradle.kts b/launchers/common/base/build.gradle.kts index 67c78ee33..b32ec7b71 100644 --- a/launchers/common/base/build.gradle.kts +++ b/launchers/common/base/build.gradle.kts @@ -2,9 +2,6 @@ plugins { `java-library` } -val edcVersion: String by project -val edcGroup: String by project - dependencies { // Control-Plane api(libs.edc.controlPlaneCore) @@ -36,5 +33,4 @@ dependencies { api(libs.edc.dataPlaneUtil) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/common/observability/build.gradle.kts b/launchers/common/observability/build.gradle.kts index 93aca628b..2adab2604 100644 --- a/launchers/common/observability/build.gradle.kts +++ b/launchers/common/observability/build.gradle.kts @@ -2,13 +2,9 @@ plugins { `java-library` } -val edcVersion: String by project -val edcGroup: String by project - dependencies { // Logging api(libs.edc.monitorJdkLogger) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/connectors/broker-server-ce/build.gradle.kts b/launchers/connectors/broker-server-ce/build.gradle.kts new file mode 100644 index 000000000..dff7d6313 --- /dev/null +++ b/launchers/connectors/broker-server-ce/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + `java-library` + id("application") + alias(libs.plugins.shadow) +} + +dependencies { + // Control-Plane + implementation(libs.edc.controlPlaneCore) + implementation(libs.edc.dataPlaneSelectorCore) + implementation(libs.edc.apiObservability) + implementation(libs.edc.configurationFilesystem) + implementation(libs.edc.controlPlaneAggregateServices) + implementation(libs.edc.http) + implementation(libs.edc.dsp) + implementation(libs.edc.jsonLd) + + // JDK Logger + implementation(libs.edc.monitorJdkLogger) + + // Broker Server + PostgreSQL + Flyway + implementation(project(":extensions:broker-server")) + + implementation(libs.edc.vaultFilesystem) + implementation(libs.edc.oauth2Core) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} diff --git a/launchers/connectors/broker-server-dev/build.gradle.kts b/launchers/connectors/broker-server-dev/build.gradle.kts new file mode 100644 index 000000000..89df7cb7e --- /dev/null +++ b/launchers/connectors/broker-server-dev/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + `java-library` + id("application") + alias(libs.plugins.shadow) +} + +dependencies { + // Control-Plane + implementation(libs.edc.controlPlaneCore) + implementation(libs.edc.dataPlaneSelectorCore) + implementation(libs.edc.apiObservability) + implementation(libs.edc.configurationFilesystem) + implementation(libs.edc.controlPlaneAggregateServices) + implementation(libs.edc.http) + implementation(libs.edc.dsp) + implementation(libs.edc.jsonLd) + + // JDK Logger + implementation(libs.edc.monitorJdkLogger) + + // Broker Server + PostgreSQL + Flyway + implementation(project(":extensions:broker-server")) + + // Connector-To-Connector IAM + implementation(libs.edc.iamMock) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} diff --git a/launchers/connectors/mds-ce/build.gradle.kts b/launchers/connectors/mds-ce/build.gradle.kts index 9ba337461..1698e71d5 100644 --- a/launchers/connectors/mds-ce/build.gradle.kts +++ b/launchers/connectors/mds-ce/build.gradle.kts @@ -4,9 +4,6 @@ plugins { alias(libs.plugins.shadow) } -val edcVersion: String by project -val edcGroup: String by project - dependencies { api(project(":launchers:common:base")) api(project(":launchers:common:base-mds")) @@ -23,5 +20,4 @@ tasks.withType { archiveFileName.set("app.jar") } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/connectors/sovity-ce/build.gradle.kts b/launchers/connectors/sovity-ce/build.gradle.kts index e0eb6d707..d2a570d2b 100644 --- a/launchers/connectors/sovity-ce/build.gradle.kts +++ b/launchers/connectors/sovity-ce/build.gradle.kts @@ -4,9 +4,6 @@ plugins { alias(libs.plugins.shadow) } -val edcVersion: String by project -val edcGroup: String by project - dependencies { api(project(":launchers:common:base")) api(project(":launchers:common:auth-daps")) @@ -22,5 +19,4 @@ tasks.withType { archiveFileName.set("app.jar") } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/connectors/sovity-dev/build.gradle.kts b/launchers/connectors/sovity-dev/build.gradle.kts index df05375fa..c1d619f2c 100644 --- a/launchers/connectors/sovity-dev/build.gradle.kts +++ b/launchers/connectors/sovity-dev/build.gradle.kts @@ -19,5 +19,4 @@ tasks.withType { archiveFileName.set("app.jar") } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/launchers/connectors/test-backend/build.gradle.kts b/launchers/connectors/test-backend/build.gradle.kts index d2ad0609b..60a27b971 100644 --- a/launchers/connectors/test-backend/build.gradle.kts +++ b/launchers/connectors/test-backend/build.gradle.kts @@ -1,5 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project plugins { `java-library` @@ -8,7 +6,7 @@ plugins { } dependencies { - api("${edcGroup}:connector-core:${edcVersion}") + api(libs.edc.connectorCore) api(libs.edc.boot) api(libs.edc.http) api(libs.edc.apiObservability) @@ -24,5 +22,4 @@ tasks.withType { archiveFileName.set("app.jar") } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() diff --git a/launchers/logging.properties b/launchers/logging.properties index b4d12f28f..17dfd8a75 100644 --- a/launchers/logging.properties +++ b/launchers/logging.properties @@ -5,3 +5,4 @@ java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s %6$s%n org.eclipse.dataspaceconnector.level = FINE org.eclipse.dataspaceconnector.handler = java.util.logging.ConsoleHandler +org.eclipse.edc.api.observability.ObservabilityApiController.level = ERROR diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b7c79e17..b4c3bf3b3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,10 @@ rootProject.name = "edc-extensions" +include(":connector") +include(":extensions:broker-server") +include(":extensions:broker-server-api:api") +include(":extensions:broker-server-api:client") +include(":extensions:broker-server-postgres-flyway-jooq") include(":extensions:edc-ui-config") include(":extensions:last-commit-info") include(":extensions:policy-always-true") @@ -21,6 +26,8 @@ include(":launchers:common:auth-mock") include(":launchers:common:base") include(":launchers:common:base-mds") include(":launchers:common:observability") +include(":launchers:connectors:broker-server-ce") +include(":launchers:connectors:broker-server-dev") include(":launchers:connectors:mds-ce") include(":launchers:connectors:sovity-ce") include(":launchers:connectors:sovity-dev") @@ -30,3 +37,4 @@ include(":utils:catalog-parser") include(":utils:json-and-jsonld-utils") include(":utils:test-connector-remote") include(":utils:test-utils") +include(":utils:versions") diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 1652ef3b8..9c6c28473 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -3,15 +3,6 @@ plugins { alias(libs.plugins.retry) } -val assertj: String by project -val edcVersion: String by project -val edcGroup: String by project -val httpMockServerVersion: String by project -val jsonUnit: String by project -val jupiterVersion: String by project -val lombokVersion: String by project -val mockitoVersion: String by project - dependencies { api(project(":launchers:common:base")) api(project(":launchers:common:auth-mock")) @@ -35,5 +26,4 @@ tasks.withType { maxParallelForks = 1 } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() diff --git a/utils/catalog-parser/build.gradle.kts b/utils/catalog-parser/build.gradle.kts index 7e1ca9a98..daf3707ce 100644 --- a/utils/catalog-parser/build.gradle.kts +++ b/utils/catalog-parser/build.gradle.kts @@ -1,10 +1,3 @@ -val lombokVersion: String by project - -val edcGroup: String by project -val edcVersion: String by project -val assertj: String by project -val mockitoVersion: String by project -val jakartaJsonVersion: String by project plugins { `java-library` @@ -37,8 +30,7 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() publishing { publications { diff --git a/utils/json-and-jsonld-utils/build.gradle.kts b/utils/json-and-jsonld-utils/build.gradle.kts index b31e53c36..d2ef943f4 100644 --- a/utils/json-and-jsonld-utils/build.gradle.kts +++ b/utils/json-and-jsonld-utils/build.gradle.kts @@ -1,10 +1,3 @@ -val lombokVersion: String by project - -val edcGroup: String by project -val edcVersion: String by project -val assertj: String by project -val mockitoVersion: String by project -val jakartaJsonVersion: String by project plugins { `java-library` @@ -32,8 +25,7 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -val sovityEdcGroup: String by project -group = sovityEdcGroup +group = libs.versions.sovityEdcGroup.get() publishing { publications { diff --git a/utils/test-connector-remote/build.gradle.kts b/utils/test-connector-remote/build.gradle.kts index 0d3e18295..a3d3d6474 100644 --- a/utils/test-connector-remote/build.gradle.kts +++ b/utils/test-connector-remote/build.gradle.kts @@ -1,10 +1,3 @@ -val edcVersion: String by project -val edcGroup: String by project -val testcontainersVersion: String by project -val lombokVersion: String by project -val restAssured: String by project -val awaitilityVersion: String by project -val assertj: String by project plugins { `java-library` @@ -20,6 +13,7 @@ dependencies { api(libs.edc.junit) api(libs.awaitility.java) api(project(":utils:json-and-jsonld-utils")) + implementation(project(":utils:versions")) implementation(libs.edc.sqlCore) implementation(libs.edc.jsonLdSpi) implementation(libs.edc.jsonLd) @@ -30,8 +24,7 @@ dependencies { implementation(libs.restAssured.restAssured) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java index 5d25b1613..8080f060b 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java @@ -14,6 +14,7 @@ package de.sovity.edc.extension.e2e.db; +import de.sovity.edc.utils.versions.GradleVersions; import org.junit.jupiter.api.extension.ExtensionContext; import org.testcontainers.containers.PostgreSQLContainer; @@ -25,7 +26,7 @@ public class TestDatabaseViaTestcontainers implements TestDatabase { private final PostgreSQLContainer container; public TestDatabaseViaTestcontainers() { - container = new PostgreSQLContainer<>("postgres:15-alpine") + container = new PostgreSQLContainer<>(GradleVersions.POSTGRES_IMAGE_TAG) .withUsername(POSTGRES_USER) .withPassword(POSTGRES_PASSWORD) .withDatabaseName(POSTGRES_DB); diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index a58a24a96..5aae2ca05 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -1,21 +1,13 @@ -val edcVersion: String by project -val edcGroup: String by project -val testcontainersVersion: String by project -val lombokVersion: String by project -val restAssured: String by project -val awaitilityVersion: String by project -val assertj: String by project plugins { `java-library` } dependencies { - api("org.junit.jupiter:junit-jupiter-api:5.10.0") + api(libs.junit.api) } -val sovityEdcExtensionGroup: String by project -group = sovityEdcExtensionGroup +group = libs.versions.sovityEdcExtensionGroup.get() publishing { publications { diff --git a/utils/versions/build.gradle.kts b/utils/versions/build.gradle.kts new file mode 100644 index 000000000..2d27f8408 --- /dev/null +++ b/utils/versions/build.gradle.kts @@ -0,0 +1,57 @@ +import com.squareup.javapoet.FieldSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeName +import com.squareup.javapoet.TypeSpec +import javax.lang.model.element.Modifier.FINAL +import javax.lang.model.element.Modifier.PUBLIC +import javax.lang.model.element.Modifier.STATIC +import java.lang.String as JavaString + + +plugins { + `java-library` +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +buildscript { + dependencies { + classpath(libs.squareup.javapoet) + } +} + +val generateGradleVersions by tasks.creating { + val generatedSourcesTarget = project.layout.buildDirectory.file("generated/sources/gradle/main/java") + doLast { + val versionsClass = TypeSpec.classBuilder("GradleVersions") + .addModifiers(PUBLIC, FINAL) + .addField( + FieldSpec.builder(TypeName.get(JavaString::class.java), "POSTGRES_IMAGE_TAG") + .initializer("\$S", libs.versions.postgresDbImage.get()) + .addModifiers(PUBLIC, STATIC, FINAL) + .build() + ) + .build() + val packageName = "de.sovity.edc.utils.versions" + val javaFile = JavaFile.builder(packageName, versionsClass) + .build() + + val target = file( + generatedSourcesTarget + ) + javaFile.writeTo(target) + } + sourceSets["main"].java.srcDir(generatedSourcesTarget) +} + +tasks.getByName("compileJava") { + dependsOn(generateGradleVersions) +} + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} From cfc2f6c56ba19293e3549c4922ad08bab7c3590d Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 29 May 2024 16:16:41 +0200 Subject: [PATCH 230/295] Fix method name --- .../edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java index bf32c520c..66cecc07d 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java @@ -43,7 +43,7 @@ void setUp(EdcExtension extension) { */ @DisabledOnGithub @Test - void test_Distribution_Key() { + void testDistributionKey() { // arrange createAsset(); createPolicy(); From 2c5a771645f02027cda8b5d2d9abfc4d50478da3 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 5 Jun 2024 14:47:48 +0200 Subject: [PATCH 231/295] chore: release prep v8.0.0 (#960) --- .env | 6 +++--- .github/ISSUE_TEMPLATE/release.md | 2 ++ CHANGELOG.md | 31 +++++++++++++++++++++++++------ archived/broker/README.md | 2 +- docker-compose-dev.yaml | 15 +++++++++++++++ docker-compose.yaml | 4 ++++ 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/.env b/.env index 86d1ac3ee..1e4caa192 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:7.5.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:7.5.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:8.0.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:8.0.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 EDC_UI_ACTIVE_PROFILE=sovity-open-source -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:4.2.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:8.0.0 diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index f9fc041b3..2887a4154 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -46,6 +46,8 @@ Feel free to edit this release checklist in-progress depending on what tasks nee the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] Set the version for `TEST_BACKEND_IMAGE` of the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + - [ ] Set the version for `BROKER_IMAGE` of + the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] Set the UI release version for `EDC_UI_IMAGE` of the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - [ ] If the Eclipse EDC version changed, update diff --git a/CHANGELOG.md b/CHANGELOG.md index 4569d8a08..b6c4a2bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Overview +### EDC UI + +### EDC Extensions and Broker + +#### Major Changes + +#### Minor Changes + +#### Patch Changes + +### Deployment Migration Notes + +## [8.0.0] - 2024-06-05 + +### Overview + Starting from version `8`, the Broker has been merged with the Community edition. [The former changelog](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for the Broker is still available but will not be updated anymore. @@ -17,17 +33,20 @@ The functionalities of each part, Broker and Extensions, on this release, is the ### EDC UI -### EDC Extensions and Broker - -#### Major Changes - -#### Minor Changes +- https://github.com/sovity/edc-ui/releases/tag/v3.2.2 #### Patch Changes - Overhaul of the Postman-Collection -### Deployment Migration Notes +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:8.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:8.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:8.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` + ## [7.5.0] - 2024-05-16 diff --git a/archived/broker/README.md b/archived/broker/README.md index 99582f89b..4bf00c932 100644 --- a/archived/broker/README.md +++ b/archived/broker/README.md @@ -116,7 +116,7 @@ Mid-development it might be un-pinned back to latest versions. ## Releasing -[Create a Release Issue](https://github.com/sovity/edc-broker-server-extension/issues/new?assignees=&labels=task%2Frelease%2Cscope%2Fmds&projects=&template=release.md&title=Release+x.x.x) and follow the instructions. +[Create a Release Issue](https://github.com/sovity/edc-extensions/issues/new?assignees=&labels=task%2Frelease%2Cscope%2Fmds&projects=&template=release.md&title=Release+x.x.x) and follow the instructions.

      (back to top)

      diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index a4eb98afd..0789fdcfb 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -159,6 +159,11 @@ services: - '54321:5432' volumes: - 'postgresql:/bitnami/postgresql' + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U edc" ] + interval: 1s + timeout: 5s + retries: 10 postgresql2: image: docker.io/bitnami/postgresql:15 @@ -171,6 +176,11 @@ services: - '54322:5432' volumes: - 'postgresql2:/bitnami/postgresql' + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U edc" ] + interval: 1s + timeout: 5s + retries: 10 broker-postgresql: image: docker.io/bitnami/postgresql:15 @@ -183,6 +193,11 @@ services: - '54323:5432' volumes: - 'broker-postgresql:/bitnami/postgresql' + healthcheck: + test: ["CMD-SHELL", "pg_isready -U edc"] + interval: 1s + timeout: 5s + retries: 10 volumes: postgresql: diff --git a/docker-compose.yaml b/docker-compose.yaml index c776e7f9e..0789fdcfb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -116,6 +116,10 @@ services: depends_on: broker-postgresql: condition: service_healthy + edc: + condition: service_started + edc2: + condition: service_started environment: EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://edc:11003/api/dsp,http://edc2:11003/api/dsp" EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" From 17b895a7a8b0bc9d9b274bb76faa084ff3145592 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 5 Jun 2024 16:37:59 +0200 Subject: [PATCH 232/295] chore: fix the Broker's NPM publishing (#961) * Merge releasing procedure and fix CI for NPM artifact publishing * remove release doc zip and MDS comms from release.md * make broker TS client library build use a different NPM PAT Co-authored-by: Richard Treier --- .github/ISSUE_TEMPLATE/release.md | 10 ++-- .github/workflows/ci.yml | 4 ++ conflicts/.github/ISSUE_TEMPLATE/release.md | 51 --------------------- 3 files changed, 11 insertions(+), 54 deletions(-) delete mode 100644 conflicts/.github/ISSUE_TEMPLATE/release.md diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 2887a4154..2562e1953 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -58,9 +58,13 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Validate the image - [ ] Pull the latest latest edc-dev image: `docker image pull ghcr.io/sovity/edc-dev:latest`. - [ ] Check that your image was built recently `docker image ls | grep ghcr.io/sovity/edc-dev`. - - [ ] Test the release `docker-compose.yaml` with `EDC_IMAGE=ghcr.io/sovity/edc-dev:latest` (at minimum execute a transfer between the two connectors). - - [ ] Test with `EDC_UI_ACTIVE_PROFILE=sovity-open-source` - - [ ] Test with `EDC_UI_ACTIVE_PROFILE=mds-open-source` + - [ ] Test the release `docker-compose.yaml` with `EDC_IMAGE=ghcr.io/sovity/edc-dev:latest` and `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest` (at minimum execute a transfer between the two connectors). + - EDC + - [ ] Test with `EDC_UI_ACTIVE_PROFILE=sovity-open-source` + - [ ] Test with `EDC_UI_ACTIVE_PROFILE=mds-open-source` + - Broker + - [ ] Validate that the EDC is scanned. + - [ ] Validate that the index is searchable. - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. - [ ] Test the [postman collection](../../docs/api/postman_collection.json) against that running docker-compose. - [ ] [Create a release](https://github.com/sovity/edc-extensions/releases/new) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b2e8e09..9dbf0dc93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,6 +235,7 @@ jobs: npm publish --access public --tag "${{ env.DIST_TAG }}" env: NODE_USER: richardtreier-sovity +<<<<<<< HEAD NODE_AUTH_TOKEN: ${{ secrets.SOVITY_BROKER_SERVER_CLIENT_NPM_AUTH }} markdown-link-checks: name: Markdown Link Checks @@ -251,3 +252,6 @@ jobs: config-file: '.github/markdown-link-checker-config.json' ======= >>>>>>> refs/rewritten/main +======= + NODE_AUTH_TOKEN: ${{ secrets.SOVITY_BROKER_CLIENT_NPM_AUTH }} +>>>>>>> a68f9ccb (chore: fix the Broker's NPM publishing (#961)) diff --git a/conflicts/.github/ISSUE_TEMPLATE/release.md b/conflicts/.github/ISSUE_TEMPLATE/release.md deleted file mode 100644 index 6852a9846..000000000 --- a/conflicts/.github/ISSUE_TEMPLATE/release.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: Release -about: Create an issue to track a release process. -title: "Release vx.x.x" -labels: ["task/release", "scope/mds"] -assignees: "" ---- - -# Release - -## Work Breakdown - -Feel free to edit this release checklist in-progress depending on what tasks need to be done: - -- [ ] Release [edc-ui](https://github.com/sovity/edc-ui), this might require several steps. -- [ ] Release [edc-extensions](https://github.com/sovity/edc-extensions), this might require several steps. -- [ ] Decide a release version depending on major/minor/patch changes in the CHANGELOG.md. -- [ ] Update this issue's title to the new version -- [ ] `release-prep` PR: - - [ ] Update the CHANGELOG.md. - - [ ] Add a clean `Unreleased` version. - - [ ] Add the version to the old section. - - [ ] Add the current date to the old version. - - [ ] Write or review the `Deployment Migration Notes` section. - - [ ] Ensure the `Deployment Migration Notes` contains the compatible docker images. - - [ ] Write or review a release summary. - - [ ] Remove empty sections from the patch notes. - - [ ] Update the [gradle.properties](https://github.com/sovity/edc-broker-server-extension/blob/main/gradle.properties) to contain the released edc-extensions version. - - [ ] Set the broker server release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - - [ ] Set the EDC UI release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - - [ ] Set the EDC CE release version in the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). - - [ ] Merge the `release-prep` PR. -- [ ] Wait for the main branch to be green. -- [ ] Test the `docker-compose.yaml` with `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:main`. -- [ ] Create a release and re-use the changelog section as release description, and the version as title. -- [ ] Check if the pipeline built the release versions in the [Actions-Section](https://github.com/sovity/edc-extensions/actions?query=event%3Arelease) (or you won't see it). -- [ ] Checkout the release tag and check test the `docker-compose.yaml`. - - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. -- [ ] Check the contents of the Deployment Docs Zip from the GitHub Release. -- [ ] Send out a release notification E-Mail to the MDS, the MDS integrator company and the MDS operator company. - - [ ] Check @jkbquabeck for an up-to-date mailing list, separated into "To" and "Cc". - - [ ] Attach the Deployment Docs Zip generated during the GitHub release, which should now contain the CHANGELOG, deployment migration notes, an initial deployment guide and a local demo docker compose. -- [ ] Optional, this can be done mid-development if required: - - [ ] Create a `release-cleanup` PR. - - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the EDC UI. - - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the EDC CE. - - [ ] Revert the versions in the [docker-compose's .env file](../../../.env) back to latest for the Broker Server. - - [ ] Update the [gradle.properties](https://github.com/sovity/edc-extensions/blob/main/gradle.properties) to contain the edc-extensions version `0.0.1-SNAPSHOT`. - - [ ] Merge the `release-cleanup` PR. -- [ ] Revisit the changed list of tasks and compare it with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-extensions/blob/main/.github/ISSUE_TEMPLATE/release.md). Apply changes where it makes sense. -- [ ] Close this issue. From 2c960762babf2d75de91068d7f074a26cb13f736 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 6 Jun 2024 09:42:17 +0200 Subject: [PATCH 233/295] fix: health indicator and version (#962) * change the health indicator to be compatible with both the broker and the extensions * Use the latest EDC version --- gradle/libs.versions.toml | 2 +- launchers/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02bc6326e..89770a97d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ commonsCompress = "1.26.1" commonsCollections = "4.4" commonsIo = "2.13.0" commonsLang = "3.13.0" -edc = "0.2.1.2" +edc = "0.2.1.3" findbugs = "3.0.2" flexmark = "0.64.8" flyway = "9.0.1" diff --git a/launchers/Dockerfile b/launchers/Dockerfile index b1e492b9b..06d01939c 100644 --- a/launchers/Dockerfile +++ b/launchers/Dockerfile @@ -36,4 +36,4 @@ ENTRYPOINT ["/app/entrypoint.sh"] CMD ["start"] # health status is determined by the availability of the /health endpoint -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11001/backend/api/check/health +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11001/api/check/health || curl -H "x-api-key: $EDC_API_AUTH_KEY" --fail http://localhost:11001/backend/api/check/health From 40804c93e90abd536a19ebdcd70ba96e12f98e9b Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 6 Jun 2024 14:00:33 +0200 Subject: [PATCH 234/295] Update release procedure (#963) --- .github/ISSUE_TEMPLATE/release.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 2562e1953..d535389ec 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -56,7 +56,7 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Merge the `release-prep` PR. - [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-extensions/actions). - [ ] Validate the image - - [ ] Pull the latest latest edc-dev image: `docker image pull ghcr.io/sovity/edc-dev:latest`. + - [ ] Pull the latest edc-dev image: `docker image pull ghcr.io/sovity/edc-dev:latest`. - [ ] Check that your image was built recently `docker image ls | grep ghcr.io/sovity/edc-dev`. - [ ] Test the release `docker-compose.yaml` with `EDC_IMAGE=ghcr.io/sovity/edc-dev:latest` and `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest` (at minimum execute a transfer between the two connectors). - EDC @@ -66,7 +66,6 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Validate that the EDC is scanned. - [ ] Validate that the index is searchable. - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. -- [ ] Test the [postman collection](../../docs/api/postman_collection.json) against that running docker-compose. - [ ] [Create a release](https://github.com/sovity/edc-extensions/releases/new) - [ ] In `Choose the tag`, type your new release version in the format `vx.y.z` (for instance `v1.2.3`) then click `+Create new tag vx.y.z on release`. - [ ] Re-use the changelog section as release description, and the version as title. From 5d97b00c9f9ef97d990bc6613344d0c4760ae594 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 7 Jun 2024 11:04:31 +0200 Subject: [PATCH 235/295] fix: Add Broker version (#964) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c4a2bc4..2e84c14df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ The functionalities of each part, Broker and Extensions, on this release, is the - Dev EDC: `ghcr.io/sovity/edc-dev:8.0.0` - sovity EDC CE: `ghcr.io/sovity/edc-ce:8.0.0` - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:8.0.0` + - Broker CE: `ghcr.io/sovity/broker-server-ce:8.0.0` - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` From 912eed9232c484077116196ac9fcee5b7a2cf226 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:23:36 +0200 Subject: [PATCH 236/295] docs: update postman_collection (#966) * docs: update postman_collection * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ docs/api/postman_collection.json | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e84c14df..c6c5846d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes +- Postman-collection: Fixed an issue where an API-call was previously wrong in the details of the POST-body. + ### Deployment Migration Notes ## [8.0.0] - 2024-06-05 diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 181dfc094..9e24a66bd 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "b6575425-f8be-4166-8c3e-cc7361909b9c", + "_postman_id": "c01dc51f-eb36-43db-b8db-a33ff2f5baa5", "name": "sovity EDC Community Edition", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-extensions](https://github.com/sovity/edc-extensions)\n\nLicense: [https://github.com/sovity/edc-extensions/blob/main/LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "32949497" + "_exporter_id": "31514741" }, "item": [ { @@ -712,7 +712,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -726,7 +727,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"12345mds\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"http://www.w3.org/ns/dcat#mediaType\": \"text/plain\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n },\n \"https://w3id.org/mobilitydcat-ap/transport-mode\": \"Road\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category\": \"Traffic Information\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category\": \"Hazard Warnings\",\n \"https://w3id.org/mobilitydcat-ap/mobility-data-standard\": \"CSV\",\n \"https://w3id.org/mobilitydcat-ap/georeferencing-method\": \"Geo Ref Method Test\"\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.sovity.de\"\n }\n}", + "raw": "{\n \"@type\": \"https://w3id.org/edc/v0.0.1/ns/Asset\",\n \"https://w3id.org/edc/v0.0.1/ns/properties\": {\n \"https://w3id.org/edc/v0.0.1/ns/id\": \"123456789mds\",\n \"http://www.w3.org/ns/dcat#version\": \"1.0\",\n \"http://purl.org/dc/terms/language\": \"https://w3id.org/idsa/code/EN\",\n \"http://purl.org/dc/terms/title\": \"test-document\",\n \"http://purl.org/dc/terms/description\": \"my test document\",\n \"http://www.w3.org/ns/dcat#keyword\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"https://w3id.org/mobilitydcat-ap/mobilityTheme\": {\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category\": \"Roadworks and Road Conditions\",\n \"https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category\": \"Road Conditions\"\n },\n \"https://w3id.org/mobilitydcat-ap/georeferencingMethod\": \"Lat/Lon\",\n \"https://w3id.org/mobilitydcat-ap/transportMode\": \"Road\",\n \"http://purl.org/dc/terms/rightsHolder\": \"my-sovereign-legal-name\",\n \"http://www.w3.org/ns/dcat#distribution\": {\n \"http://www.w3.org/ns/dcat#mediaType\": \"application/json\",\n \"https://w3id.org/mobilitydcat-ap/mobilityDataStandard\": {\n \"@id\": \"my-data-model-001\",\n \"https://w3id.org/mobilitydcat-ap/schema\": {\n \"http://www.w3.org/ns/dcat#downloadURL\": [\n \"https://teamabc.departmentxyz.schema/a\",\n \"https://teamabc.departmentxyz.schema/b\"\n ],\n \"http://www.w3.org/2000/01/rdf-schema#Literal\": \"These reference files are important\"\n }\n },\n \"http://www.w3.org/ns/adms#sample\": [\n \"https://teamabc.departmentxyz.sample/a\",\n \"https://teamabc.departmentxyz.sample/b\"\n ],\n \"http://purl.org/dc/terms/rights\": {\n \"http://www.w3.org/2000/01/rdf-schema#label\": \"Please cite the dataset as...\"\n }\n },\n \"http://purl.org/dc/terms/accrualPeriodicity\": \"every month\",\n \"http://purl.org/dc/terms/temporal\": {\n \"http://www.w3.org/ns/dcat#startDate\": \"2024-02-01\",\n \"http://www.w3.org/ns/dcat#endDate\": \"2024-02-10\"\n },\n \"http://purl.org/dc/terms/creator\": {\n \"http://xmlns.com/foaf/0.1/name\": \"My Org\"\n },\n \"http://purl.org/dc/terms/license\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"http://www.w3.org/ns/dcat#landingPage\": \"https://mydepartment.myorg.com/my-offer\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams\": \"false\",\n \"https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody\": \"false\",\n \"http://purl.org/dc/terms/publisher\": {\n \"http://xmlns.com/foaf/0.1/homepage\": \"https://myorg.com/\"\n }\n },\n \"https://w3id.org/edc/v0.0.1/ns/privateProperties\": {},\n \"https://w3id.org/edc/v0.0.1/ns/dataAddress\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.sovity.de\"\n }\n}", "options": { "raw": { "language": "json" @@ -1921,4 +1922,4 @@ "type": "default" } ] -} \ No newline at end of file +} From f97ca616cf83a0c325e0cbdc0e4f4484848adf42 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Tue, 11 Jun 2024 11:25:19 +0200 Subject: [PATCH 237/295] docs: fix wrong port in local demo docs (#967) --- docs/deployment-guide/goals/local-demo/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/deployment-guide/goals/local-demo/README.md b/docs/deployment-guide/goals/local-demo/README.md index f5c3e26cd..fe90159a3 100644 --- a/docs/deployment-guide/goals/local-demo/README.md +++ b/docs/deployment-guide/goals/local-demo/README.md @@ -54,12 +54,12 @@ EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up The default configuration launches two local EDC Connectors and a Broker with the following credentials: -| | First Connector | Second Connector | Broker | -|---------------------|---------------------------------------------------------------|:------------------------------------------------------------------------|------------------------------------------------------------------| -| Homepage | http://localhost:11000 | http://localhost:22000 | http://localhost:44000 | -| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | http://localhost:44002/api/management | -| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | -| Connector Endpoint | http://edc:11003/api/dsp
      Requires Docker Compose Network | http://edc2:22003/api/dsp
      Requires Docker Compose Network | http://broker:11003/api/dsp
      Requires Docker Compose Network | +| | First Connector | Second Connector | Broker | +|---------------------|---------------------------------------------------------------|:---------------------------------------------------------------|------------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | http://localhost:44000 | +| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | http://localhost:44002/api/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://edc:11003/api/dsp
      Requires Docker Compose Network | http://edc2:11003/api/dsp
      Requires Docker Compose Network | http://broker:11003/api/dsp
      Requires Docker Compose Network | The Broker is configured to scan both connectors. From 9dd48dcaf544daaa0f637a7aeda19c5f19785183 Mon Sep 17 00:00:00 2001 From: Eric Fiege <105237007+efiege@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:01:29 +0200 Subject: [PATCH 238/295] feat: complex policy expressions (API Wrapper Use Case API, #969) --- CHANGELOG.md | 4 + docs/api/sovity-edc-api-wrapper.yaml | 82 ++++++++++++ .../clients/java-client/build.gradle.kts | 1 + .../wrapper/api/usecase/UseCaseResource.java | 9 ++ .../usecase/model/PolicyCreateRequest.java | 41 ++++++ .../wrapper/api/common/model/Expression.java | 49 ++++++++ .../api/common/model/ExpressionDto.java | 49 -------- .../api/common/model/ExpressionType.java | 10 +- .../api/common/model/PermissionDto.java | 2 +- .../wrapper-common-mappers/build.gradle.kts | 1 + .../api/common/mappers/PolicyMapper.java | 45 +++++++ .../mappers/utils/AtomicConstraintMapper.java | 14 +++ .../api/common/mappers/PolicyMapperTest.java | 35 ++++++ .../WrapperExtensionContextBuilder.java | 3 +- .../policy/PolicyDefinitionApiService.java | 16 +++ .../api/usecase/UseCaseResourceImpl.java | 9 ++ .../PolicyDefinitionApiServiceTest.java | 13 +- .../PolicyDefinitionApiServiceTest.java | 117 ++++++++++++++++++ .../sovity/edc/utils/jsonld/vocab/Prop.java | 8 ++ 19 files changed, 452 insertions(+), 56 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java create mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c5846d6..2ca3c4d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- API Wrapper + - Support for Multiplicity Constraints (https://github.com/sovity/edc-extensions/issues/968) + - Providing `Prop` class from `json-and-jsonld-utils` to the java-client to make relevant Constants available + #### Patch Changes - Postman-collection: Fixed an issue where an API-call was previously wrong in the details of the POST-body. diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 15670f25f..03747c25a 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -408,6 +408,24 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' + /wrapper/use-case-api/policy-definition: + post: + tags: + - Use Case + description: Create a new Policy Definition + operationId: createPolicyDefinitionUseCase + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyCreateRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' /wrapper/use-case-api/kpis: get: tags: @@ -1547,6 +1565,70 @@ components: description: Additional transfer process properties. These are not passed to the consumer EDC description: "For type PARAMS_ONLY: Required data for starting a Transfer Process" + AtomicConstraintDto: + required: + - leftExpression + - operator + - rightExpression + type: object + properties: + leftExpression: + type: string + description: Left part of the constraint. + operator: + $ref: '#/components/schemas/OperatorDto' + rightExpression: + type: string + description: Right part of the constraint. + description: Type-Safe OpenAPI generator friendly Constraint DTO that supports + an opinionated subset of the original EDC Constraint Entity. + Expression: + type: object + properties: + expressionType: + $ref: '#/components/schemas/ExpressionType' + expressions: + type: array + description: List of policy elements that are evaluated according the constraintType. + items: + $ref: '#/components/schemas/Expression' + atomicConstraint: + $ref: '#/components/schemas/AtomicConstraintDto' + description: Generic Policy Element. Represents a single atomic constraint or + a multiplicity constraint. The atomicConstraint will be evaluated if the constraintType + is ATOMIC. + ExpressionType: + type: string + description: | + Expression types: + * `ATOMIC_CONSTRAINT` - A single constraint for the policy + * `AND` - Several constraints, all of which must be respected + * `OR` - Several constraints, of which at least one must be respected + * `XOR` - Several constraints, of which exactly one must be respected + enum: + - ATOMIC_CONSTRAINT + - AND + - OR + - XOR + PermissionDto: + required: + - expression + type: object + properties: + expression: + $ref: '#/components/schemas/Expression' + description: Permission description for the policy to evaluate to TRUE. + PolicyCreateRequest: + required: + - policyDefinitionId + type: object + properties: + policyDefinitionId: + type: string + description: Policy Definition ID + permission: + $ref: '#/components/schemas/PermissionDto' + description: Policy Creation Request Supporting Multiplicity Constraints. KpiResult: required: - assetsCount diff --git a/extensions/wrapper/clients/java-client/build.gradle.kts b/extensions/wrapper/clients/java-client/build.gradle.kts index 65a99230f..1f86c6774 100644 --- a/extensions/wrapper/clients/java-client/build.gradle.kts +++ b/extensions/wrapper/clients/java-client/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(libs.openapi.jacksonDatabindNullable) implementation(libs.apache.commonsLang) implementation(libs.jakarta.annotationApi) + api(project(":utils:json-and-jsonld-utils")) // Lombok compileOnly(libs.lombok) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java index 988f89bef..459ed44ed 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -14,6 +14,8 @@ package de.sovity.edc.ext.wrapper.api.usecase; +import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; @@ -59,4 +61,11 @@ List queryCatalog( @Valid @NotNull CatalogQuery catalogQuery ); + + @POST + @Path("policy-definition") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new Policy Definition") + IdResponseDto createPolicyDefinitionUseCase(PolicyCreateRequest policyCreateRequest); } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java new file mode 100644 index 000000000..3cd2d6ac3 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.usecase.model; + +import de.sovity.edc.ext.wrapper.api.common.model.PermissionDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = "Policy Creation Request Supporting Multiplicity Constraints.") +public class PolicyCreateRequest { + + @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String policyDefinitionId; + + @Schema(description = "Permission description for the policy to evaluate to TRUE.") + private PermissionDto permission; + +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java new file mode 100644 index 000000000..07e7adb65 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Builder(toBuilder = true) +@RequiredArgsConstructor +@Schema(description = + "Represents a single atomic constraint or a multiplicity constraint. The atomicConstraint" + + " will be evaluated if the constraintType is ATOMIC_CONSTRAINT.") +public class Expression { + + @Schema(description = "Either ATOMIC_CONSTRAINT or one of the multiplicity constraint types.") + private ExpressionType expressionType; + + @Schema(description = + "List of policy elements that are evaluated according the expressionType.") + private List expressions; + + @Schema(description = + "A single atomic constraint. Will be evaluated if the expressionType is set to " + + "ATOMIC_CONSTRAINT.") + private AtomicConstraintDto atomicConstraint; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java deleted file mode 100644 index 17494b6f3..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionDto.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - */ - -package de.sovity.edc.ext.wrapper.api.common.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * Expression constraints for policies. - * - * @author tim.dahlmanns@isst.fraunhofer.de - */ -@Getter -@Builder(toBuilder = true) -@NoArgsConstructor -@AllArgsConstructor -public class ExpressionDto { - - @Schema(description = """ - Expression types: - * `EMPTY` - No constraints for the policy - * `ATOMIC_CONSTRAINT` - A single constraint for the policy - * `AND` - Several constraints, all of which must be respected - * `OR` - Several constraints, of which at least one must be respected - * `XOR` - Several constraints, of which exactly one must be respected - """ - ) - private ExpressionType type; - private AtomicConstraintDto atomicConstraint; - private List and; - private List or; - private List xor; -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java index 19b9e3e08..33e21f83f 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java @@ -5,7 +5,13 @@ /** * Sum type enum. */ -@Schema(enumAsRef = true) +@Schema(description = """ + Expression types: + * `ATOMIC_CONSTRAINT` - A single constraint for the policy + * `AND` - Several constraints, all of which must be respected + * `OR` - Several constraints, of which at least one must be respected + * `XOR` - Several constraints, of which exactly one must be respected + """, enumAsRef = true) public enum ExpressionType { - EMPTY, ATOMIC_CONSTRAINT, AND, OR, XOR + ATOMIC_CONSTRAINT, AND, OR, XOR } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java index 4bcd46a9e..a29202dbe 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java @@ -35,5 +35,5 @@ public class PermissionDto { @Schema(description = "Possible constraints for the permission", requiredMode = RequiredMode.REQUIRED) - private ExpressionDto constraints; + private Expression expression; } diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts index aa42db904..d468dadee 100644 --- a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { testImplementation(libs.jsonUnit.assertj) testImplementation(libs.assertj.core) testImplementation(libs.junit.api) + testImplementation(libs.junit.params) testImplementation(libs.mockito.core) testImplementation(libs.mockito.inline) testImplementation(libs.mockito.junitJupiter) diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java index 06da1725f..81dc116f2 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java @@ -5,19 +5,26 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; +import de.sovity.edc.ext.wrapper.api.common.model.Expression; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.JsonObject; import lombok.RequiredArgsConstructor; import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.AndConstraint; import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.OrConstraint; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.policy.model.PolicyType; +import org.eclipse.edc.policy.model.XoneConstraint; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.List; import static de.sovity.edc.utils.JsonUtils.toJson; @@ -72,6 +79,44 @@ public Policy buildPolicy(UiPolicyCreateRequest policyCreateDto) { .build(); } + public Policy buildPolicy(List constraintElements) { + var constraints = buildConstraints(constraintElements); + var action = Action.Builder.newInstance().type(Prop.Odrl.USE).build(); + var permission = Permission.Builder.newInstance() + .action(action) + .constraints(constraints) + .build(); + + return Policy.Builder.newInstance() + .type(PolicyType.SET) + .permission(permission) + .build(); + } + + @NotNull + private List buildConstraints(List expressions) { + return expressions.stream() + .map(this::buildConstraint) + .toList(); + } + + private Constraint buildConstraint(Expression expression) { + var subExpressions = expression.getExpressions(); + return switch (expression.getExpressionType()) { + case ATOMIC_CONSTRAINT -> + atomicConstraintMapper.buildAtomicConstraint(expression.getAtomicConstraint()); + case AND -> AndConstraint.Builder.newInstance() + .constraints(buildConstraints(subExpressions)) + .build(); + case OR -> OrConstraint.Builder.newInstance() + .constraints(buildConstraints(subExpressions)) + .build(); + case XOR -> XoneConstraint.Builder.newInstance() + .constraints(buildConstraints(subExpressions)) + .build(); + }; + } + /** * Maps an ODRL Policy from JSON-LD to the Core EDC Type. *

      diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java index c39a26de4..896fc56e5 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java @@ -14,11 +14,13 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.utils; import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.model.AtomicConstraintDto; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Constraint; import org.eclipse.edc.policy.model.LiteralExpression; import java.util.List; @@ -103,4 +105,16 @@ private AtomicConstraint buildAtomicConstraint(UiPolicyConstraint constraint) { .rightExpression(new LiteralExpression(right)) .build(); } + + public AtomicConstraint buildAtomicConstraint(AtomicConstraintDto atomicConstraint) { + var left = atomicConstraint.getLeftExpression(); + var operator = operatorMapper.getOperator(atomicConstraint.getOperator()); + var right = atomicConstraint.getRightExpression(); + + return AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression(left)) + .operator(operator) + .rightExpression(new LiteralExpression(right)) + .build(); + } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java index 13fe9ee76..9c12919d8 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java @@ -3,6 +3,9 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.model.AtomicConstraintDto; +import de.sovity.edc.ext.wrapper.api.common.model.Expression; +import de.sovity.edc.ext.wrapper.api.common.model.ExpressionType; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; import jakarta.json.JsonObject; @@ -14,6 +17,8 @@ import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -21,8 +26,10 @@ import java.util.List; +import static de.sovity.edc.ext.wrapper.api.common.model.ExpressionType.ATOMIC_CONSTRAINT; import static de.sovity.edc.utils.JsonUtils.parseJsonObj; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -88,4 +95,32 @@ void test_buildPolicy() { assertThat(actual.getPermissions().get(0).getAction().getType()).isEqualTo("USE"); assertThat(actual.getPermissions().get(0).getConstraints().get(0)).isSameAs(expected); } + + @ParameterizedTest + @ValueSource(strings = {"AND", "OR", "XOR"}) + void buildGenericPolicy(String constraintTypeString) { + // arrange + var expressionType = ExpressionType.valueOf(constraintTypeString); + var incomingConstraint = mock(AtomicConstraintDto.class); + var mappedAtomicConstraint = mock(AtomicConstraint.class); + var atomicConstraint = new Expression(ATOMIC_CONSTRAINT, List.of(), incomingConstraint); + var atomicConstraints = List.of(atomicConstraint, atomicConstraint); + var baseConstraintElement = new Expression(expressionType, atomicConstraints, null); + + // act + when(atomicConstraintMapper + .buildAtomicConstraint(eq(incomingConstraint))) + .thenReturn(mappedAtomicConstraint); + var policy = policyMapper.buildPolicy(List.of(baseConstraintElement)); + + // assert + assertThat(policy.getType()).isEqualTo(PolicyType.SET); + assertThat(policy.getPermissions()).hasSize(1); + var permission = policy.getPermissions().get(0); + assertThat(permission.getConstraints()).hasSize(1); + assertThat(permission.getAction().getType()).isEqualTo("USE"); + + var constraintObject = permission.getConstraints().get(0); + assertNotNull(constraintObject); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 8a87a6110..8f2aa2ae8 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -289,7 +289,8 @@ public static WrapperExtensionContext buildContext( var useCaseResource = new UseCaseResourceImpl( kpiApiService, supportedPolicyApiService, - useCaseCatalogApiService + useCaseCatalogApiService, + policyDefinitionApiService ); // Collect all JAX-RS resources diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java index 21f46a5f3..e63b10170 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java @@ -17,6 +17,7 @@ import de.sovity.edc.ext.wrapper.api.ServiceException; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; @@ -75,4 +76,19 @@ public PolicyDefinition buildPolicyDefinition(PolicyDefinitionCreateRequest poli .policy(policy) .build(); } + + public IdResponseDto createPolicyDefinition(PolicyCreateRequest policyCreateRequest) { + var policyDefinition = buildPolicyDefinition(policyCreateRequest); + policyDefinition = policyDefinitionService.create(policyDefinition).orElseThrow(ServiceException::new); + return new IdResponseDto(policyDefinition.getId()); + } + + private PolicyDefinition buildPolicyDefinition(PolicyCreateRequest policyCreateRequest) { + var permissionExpression = policyCreateRequest.getPermission().getExpression(); + var policy = policyMapper.buildPolicy(List.of(permissionExpression)); + return PolicyDefinition.Builder.newInstance() + .id(policyCreateRequest.getPolicyDefinitionId()) + .policy(policy) + .build(); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java index 7f1bf7d74..c743faa64 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java @@ -14,7 +14,10 @@ package de.sovity.edc.ext.wrapper.api.usecase; +import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; +import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; @@ -33,6 +36,7 @@ public class UseCaseResourceImpl implements UseCaseResource { private final KpiApiService kpiApiService; private final SupportedPolicyApiService supportedPolicyApiService; private final UseCaseCatalogApiService useCaseCatalogApiService; + private final PolicyDefinitionApiService policyDefinitionApiService; @Override public KpiResult getKpis() { @@ -48,4 +52,9 @@ public List getSupportedFunctions() { public List queryCatalog(CatalogQuery catalogQuery) { return useCaseCatalogApiService.fetchDataOffers(catalogQuery); } + + @Override + public IdResponseDto createPolicyDefinitionUseCase(PolicyCreateRequest policyCreateRequest) { + return policyDefinitionApiService.createPolicyDefinition(policyCreateRequest); + } } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java index ad371f0ae..d9edd6444 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java @@ -59,7 +59,7 @@ void setUp(EdcExtension extension) { } @Test - void test_create_list() { + void getPolicyList() { // arrange createPolicyDefinition("my-policy-def-1"); @@ -92,7 +92,11 @@ void test_sorting(PolicyDefinitionService policyDefinitionService) { // assert assertThat(result.getPolicies()) .extracting(PolicyDefinitionDto::getPolicyDefinitionId) - .containsExactly("always-true", "my-policy-def-2", "my-policy-def-1", "my-policy-def-0"); + .containsExactly( + "always-true", + "my-policy-def-2", + "my-policy-def-1", + "my-policy-def-0"); } @Test @@ -118,7 +122,10 @@ private void createPolicyDefinition(String policyDefinitionId) { } @SneakyThrows - private void createPolicyDefinition(PolicyDefinitionService policyDefinitionService, String policyDefinitionId, long createdAt) { + private void createPolicyDefinition( + PolicyDefinitionService policyDefinitionService, + String policyDefinitionId, + long createdAt) { createPolicyDefinition(policyDefinitionId); var policyDefinition = policyDefinitionService.findById(policyDefinitionId); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java new file mode 100644 index 000000000..e411e36a2 --- /dev/null +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java @@ -0,0 +1,117 @@ +package de.sovity.edc.ext.wrapper.api.usecase; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.AtomicConstraintDto; +import de.sovity.edc.client.gen.model.Expression; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PermissionDto; +import de.sovity.edc.client.gen.model.PolicyCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionDto; +import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.StringReader; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static de.sovity.edc.client.gen.model.ExpressionType.AND; +import static de.sovity.edc.client.gen.model.ExpressionType.ATOMIC_CONSTRAINT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@ApiTest +@ExtendWith(EdcExtension.class) +public class PolicyDefinitionApiServiceTest { + + private EdcClient client; + + @BeforeEach + void setUp(EdcExtension extension) { + TestUtils.setupExtension(extension); + client = TestUtils.edcClient(); + } + + @Test + void createTraceXPolicy() { + // arrange + var policyId = UUID.randomUUID().toString(); + var membershipElement = buildAtomicElement("Membership", OperatorDto.EQ, "active"); + var purposeElement = buildAtomicElement("PURPOSE", OperatorDto.EQ, "ID 3.1 Trace"); + var andElement = new Expression() + .expressionType(AND) + .expressions(List.of(membershipElement, purposeElement)); + var permissionDto = new PermissionDto(andElement); + var createRequest = new PolicyCreateRequest(policyId, permissionDto); + + // act + var response = client.useCaseApi().createPolicyDefinitionUseCase(createRequest); + + // assert + assertThat(response.getId()).isEqualTo(policyId); + var policyById = getPolicyById(policyId); + assertThat(policyById).isPresent(); + var policyDefinitionDto = policyById.get(); + assertEquals(policyId, policyDefinitionDto.getPolicyDefinitionId()); + assertPolicyJsonLd(policyDefinitionDto); + } + + private void assertPolicyJsonLd(PolicyDefinitionDto policyDefinitionDto) { + var permission = getPermissionJsonObject(policyDefinitionDto.getPolicy().getPolicyJsonLd()); + var action = permission.get(Prop.Odrl.ACTION); + assertEquals(Prop.Odrl.USE, action.asJsonObject().getString(Prop.Odrl.TYPE)); + + var permissionConstraints = permission.get(Prop.Odrl.CONSTRAINT).asJsonArray(); + assertThat(permissionConstraints).hasSize(1); + var andConstraint = permissionConstraints.get(0).asJsonObject(); + var andConstraints = andConstraint.get(Prop.Odrl.AND).asJsonArray(); + assertThat(andConstraints).hasSize(2); + + var membershipConstraint = andConstraints.get(0).asJsonObject(); + var purposeConstraint = andConstraints.get(1).asJsonObject(); + assertAtomicConstraint(membershipConstraint, "Membership", "active"); + assertAtomicConstraint(purposeConstraint, "PURPOSE", "ID 3.1 Trace"); + } + + private static JsonObject getPermissionJsonObject(String policyJsonLdString) { + var jsonReader = Json.createReader(new StringReader(policyJsonLdString)); + var jsonObject = jsonReader.readObject(); + var permissionList = jsonObject.get(Prop.Odrl.PERMISSION); + return permissionList.asJsonArray().get(0).asJsonObject(); + } + + private void assertAtomicConstraint(JsonObject atomicConstraint, String left, String right) { + var leftOperand = atomicConstraint.getJsonObject(Prop.Odrl.LEFT_OPERAND); + assertEquals(left, leftOperand.getString("@value")); + var rightOperand = atomicConstraint.getJsonObject(Prop.Odrl.RIGHT_OPERAND); + assertEquals(right, rightOperand.getString("@value")); + } + + private Expression buildAtomicElement( + String left, + OperatorDto operator, + String right) { + var atomicConstraint = new AtomicConstraintDto() + .leftExpression(left) + .operator(operator) + .rightExpression(right); + return new Expression() + .expressionType(ATOMIC_CONSTRAINT) + .atomicConstraint(atomicConstraint); + } + + private Optional getPolicyById(String policyId) { + var policyDefinitionsResponse = client.uiApi().getPolicyDefinitionPage(); + return policyDefinitionsResponse.getPolicies().stream() + .filter(policy -> policy.getPolicyDefinitionId().equals(policyId)) + .findFirst(); + } +} diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index b9f48c84c..3606ddde9 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -87,6 +87,14 @@ public class Dcat { public class Odrl { public final String CTX = "http://www.w3.org/ns/odrl/2/"; public final String HAS_POLICY = CTX + "hasPolicy"; + public final String ACTION = CTX + "action"; + public final String TYPE = CTX + "type"; + public final String CONSTRAINT = CTX + "constraint"; + public final String AND = CTX + "and"; + public final String PERMISSION = CTX + "permission"; + public final String LEFT_OPERAND = CTX + "leftOperand"; + public final String RIGHT_OPERAND = CTX + "rightOperand"; + public final String USE = "USE"; } /** From bc254d8fa4780edcceff2d527e5c9b2e33709eb7 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:41:46 +0200 Subject: [PATCH 239/295] docs: add examples of api-wrapper java-client to readme (#971) * Update README.md --- .../wrapper/clients/java-client/README.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/extensions/wrapper/clients/java-client/README.md b/extensions/wrapper/clients/java-client/README.md index fc1c6e264..ffbe6c0a6 100644 --- a/extensions/wrapper/clients/java-client/README.md +++ b/extensions/wrapper/clients/java-client/README.md @@ -103,6 +103,54 @@ public class WrapperClientExample { } ``` +### Further Examples + +Below are the examples of various tasks and the corresponding methods to be used from the Java-client. + +| Task | Java-Client method | +|------------------------------------------------------|-------------------------------------------------------------------------| +| Create Policy - uiAPI | `EdcClient.uiApi().createPolicyDefinition(policyDefinition)` | +| Create Policy - useCaseApi (allows AND/OR/XOR operators) | `EdcClient.useCaseApi().createPolicyDefinitionUseCase(createRequest)` | +| Create asset (Asset Creation after activate) | `EdcClient.uiApi().createAsset(uiAssetRequest)` | +| Create contract definition | `EdcClient.uiApi().createContractDefinition(contractDefinition)` | +| Create Offer on consumer dashboard (Catalog Browser) | `EdcClient.uiApi().getCatalogPageDataOffers(PROTOCOL_ENDPOINT)` | +| Accept contract (Contract Negotiation) | `EdcClient.uiApi().initiateContractNegotiation(negotiationRequest)` | +| Transfer Data (Initiate Transfer) | `EdcClient.uiApi().initiateTransfer(negotiation)` | + +These methods facilitate various operations such as creating policies, assets, contract definitions, browsing offers, accepting contracts, and initiating data transfers. + +### Example Creating a Catena-Policy using operators (AND/OR/XOR) + +The following example demonstrates how to create a Catena-Policy with linked conditions using the Java-client. + +```java +var policyId = UUID.randomUUID().toString(); +var membershipElement = buildAtomicElement("Membership", OperatorDto.EQ, "active"); +var purposeElement = buildAtomicElement("PURPOSE", OperatorDto.EQ, "ID 3.1 Trace"); +var andElement = new Expression() + .expressionType(ExpressionTypeDto.AND) + .expressions(List.of(membershipElement, purposeElement)); +var permissionDto = new PermissionDto(andElement); +var createRequest = new PolicyCreateRequest(policyId, permissionDto); + +var response = client.useCaseApi().createPolicyDefinitionUseCase(createRequest); + +private Expression buildAtomicElement( + String left, + OperatorDto operator, + String right) { + var atomicConstraint = new AtomicConstraintDto() + .leftExpression(left) + .operator(operator) + .rightExpression(right); + return new Expression() + .expressionType(ExpressionTypeDto.ATOMIC_CONSTRAINT) + .atomicConstraint(atomicConstraint); +} +``` + +The complete example can be seen in [this test](https://github.com/sovity/edc-extensions/blob/main/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java). + ## License Apache License 2.0 - see [LICENSE](../../../../LICENSE) From 5e086782ef15235767f9209c03d2ec9c20e67ff1 Mon Sep 17 00:00:00 2001 From: Eric Fiege <105237007+efiege@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:08:49 +0200 Subject: [PATCH 240/295] release: 8.1.0 (#974) --- .env | 6 +++--- CHANGELOG.md | 27 ++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 1e4caa192..0aa50efab 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:8.0.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:8.0.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:8.1.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:8.1.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 EDC_UI_ACTIVE_PROFILE=sovity-open-source -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:8.0.0 +BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:8.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca3c4d21..532123774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,24 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +#### Patch Changes + +### Deployment Migration Notes + +## [8.1.0] - 2024-06-14 + +### Overview + +Support for Multiplicity Constraints in the API Wrapper. + +### EDC UI + +- https://github.com/sovity/edc-ui/releases/tag/v3.2.2 + +### EDC Extensions and Broker + +#### Minor Changes + - API Wrapper - Support for Multiplicity Constraints (https://github.com/sovity/edc-extensions/issues/968) - Providing `Prop` class from `json-and-jsonld-utils` to the java-client to make relevant Constants available @@ -23,7 +41,14 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). - Postman-collection: Fixed an issue where an API-call was previously wrong in the details of the POST-body. -### Deployment Migration Notes +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:8.1.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:8.1.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:8.1.0` + - Broker CE: `ghcr.io/sovity/broker-server-ce:8.1.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` ## [8.0.0] - 2024-06-05 From d96ec83f32e6d70715ef7f4981bd57feebd8ff7c Mon Sep 17 00:00:00 2001 From: Eric Fiege <105237007+efiege@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:59:03 +0200 Subject: [PATCH 241/295] feat: Add /wrapper/use-case-api/policy-definition endpoint to postman (#976) --- docs/api/postman_collection.json | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 9e24a66bd..3da385756 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -655,6 +655,44 @@ "response": [] } ] + }, + { + "name": "Use Case Api", + "item": [ + { + "name": "Policies", + "item": [ + { + "name": "Create Policy (And)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"policyDefinitionId\": \"testAndRestricted\",\r\n \"permission\": {\r\n \"expression\": {\r\n \"expressionType\": \"AND\",\r\n \"expressions\": [\r\n {\r\n \"expressionType\": \"ATOMIC_CONSTRAINT\",\r\n \"atomicConstraint\": {\r\n \"leftExpression\": \"Membership\",\r\n \"operator\": \"EQ\",\r\n \"rightExpression\": \"active\"\r\n }\r\n },\r\n {\r\n \"expressionType\": \"ATOMIC_CONSTRAINT\",\r\n \"atomicConstraint\": {\r\n \"leftExpression\": \"PURPOSE\",\r\n \"operator\": \"EQ\",\r\n \"rightExpression\": \"ID 3.1 Trace\"\r\n }\r\n }\r\n ]\r\n }\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/use-case-api/policy-definition", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "use-case-api", + "policy-definition" + ] + } + }, + "response": [] + } + ] + } + ] } ] }, From 44b8fae8d32b03d26bb069917db7d388c838d7a7 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Tue, 18 Jun 2024 11:22:13 +0200 Subject: [PATCH 242/295] chore: Unify migration histories (#965) * Import migration scripts from EDC EE * Move legacy migration scripts * Add MS8 as a single file migration * Set baseline for unified history to 8 * Update CHANGELOG.md --------- Co-authored-by: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> --- CHANGELOG.md | 5 + extensions/postgres-flyway/build.gradle.kts | 6 +- .../postgresql/DataSourceFactory.java | 69 +++ .../extension/postgresql/FlywayFactory.java | 68 +++ .../extension/postgresql/FlywayMigrator.java | 104 ++++ .../postgresql/PostgresFlywayConfig.java | 52 ++ .../postgresql/PostgresFlywayExtension.java | 84 ++- .../DriverManagerConnectionFactory.java | 2 +- .../connection/JdbcConnectionProperties.java | 2 +- .../migration/DatabaseMigrationManager.java | 5 +- .../{ => legacy}/migration/FlywayService.java | 9 +- .../postgresql/utils/DatabaseUtils.java | 85 +++ .../postgresql/utils/JdbcCredentials.java | 29 + .../migration/asset/V0_0_1__Asset_Schema.sql | 0 .../migration/asset/V0_0_2__Asset_Schema.sql | 0 .../V0_0_1__ContractDefinition_Schema.sql | 0 .../V0_0_2__ContractDefinition_Schema.sql | 0 .../V0_0_3__ContractDefinition_Schema.sql | 0 .../V0_0_4__Set_Default_Validity.sql | 0 .../V0_0_1__ContractNegotiation_Schema.sql | 0 .../V0_0_2__ContractNegotiation_Schema.sql | 0 .../V0_0_3__Fix_Contract_Offer_JSON.sql | 0 .../V0_0_1__DataplaneInstance_Schema.sql | 0 .../default/V1_0_0_Example_Migration.sql | 0 .../V2__Delete-Transfer-Processes-Trigger.sql | 0 .../migration/default/V3__MS8-to-0.2.1.sql | 0 .../default/V4__MS8-to-0.2.1_bugfixes.sql | 0 .../default/V5__Mobility_DCAT_Mapping.sql | 0 .../default/V6__Fix_DataModel_ID_Field.sql | 0 ..._legacy_migration_do_not_continue_here.sql | 3 + .../policy/V0_0_1__Policy_Schema.sql | 0 .../policy/V0_0_2__Policy_Schema.sql | 0 .../V0_0_1__TransferProcess_Schema.sql | 0 .../V0_0_2__TransferProcess_Schema.sql | 0 .../V0_0_3__TransferProcess_Schema.sql | 0 .../V0_0_4__Set_Default_Properties.sql | 0 .../main/resources/db/migration/V1__MS8.sql | 505 ++++++++++++++++++ .../V2__Delete-Transfer-Processes-Trigger.sql | 41 ++ .../db/migration/V3__MS8-to-0.2.1.sql | 416 +++++++++++++++ .../migration/V4__MS8-to-0.2.1_bugfixes.sql | 48 ++ .../migration/V5__Mobility_DCAT_Mapping.sql | 76 +++ .../migration/V6__Fix_DataModel_ID_Field.sql | 63 +++ .../db/migration/V9__legacy_cleanup.sql | 11 + 43 files changed, 1658 insertions(+), 25 deletions(-) create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/DataSourceFactory.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayMigrator.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayConfig.java rename extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/{ => legacy}/connection/DriverManagerConnectionFactory.java (95%) rename extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/{ => legacy}/connection/JdbcConnectionProperties.java (96%) rename extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/{ => legacy}/migration/DatabaseMigrationManager.java (92%) rename extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/{ => legacy}/migration/FlywayService.java (95%) create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/DatabaseUtils.java create mode 100644 extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/JdbcCredentials.java rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/asset/V0_0_1__Asset_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/asset/V0_0_2__Asset_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/default/V1_0_0_Example_Migration.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/default/V2__Delete-Transfer-Processes-Trigger.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/default/V3__MS8-to-0.2.1.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/default/V4__MS8-to-0.2.1_bugfixes.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/default/V5__Mobility_DCAT_Mapping.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/default/V6__Fix_DataModel_ID_Field.sql (100%) create mode 100644 extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V7__last_legacy_migration_do_not_continue_here.sql rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/policy/V0_0_1__Policy_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/policy/V0_0_2__Policy_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql (100%) rename extensions/postgres-flyway/src/main/resources/{ => db/legacy}/migration/transferprocess/V0_0_4__Set_Default_Properties.sql (100%) create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V1__MS8.sql create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V2__Delete-Transfer-Processes-Trigger.sql create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V3__MS8-to-0.2.1.sql create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V4__MS8-to-0.2.1_bugfixes.sql create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V5__Mobility_DCAT_Mapping.sql create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V6__Fix_DataModel_ID_Field.sql create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V9__legacy_cleanup.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 532123774..47082e289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,13 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes +- Unified database migration histories + ### Deployment Migration Notes +The database migration system has been moved from multiple migration history tables to a single one. Although this +process has been extensively tested in the enterprise edition already, it should be tested once more on a copy of a productive connector. + ## [8.1.0] - 2024-06-14 ### Overview diff --git a/extensions/postgres-flyway/build.gradle.kts b/extensions/postgres-flyway/build.gradle.kts index e4f6f0ba6..8b0b67dca 100644 --- a/extensions/postgres-flyway/build.gradle.kts +++ b/extensions/postgres-flyway/build.gradle.kts @@ -16,10 +16,14 @@ dependencies { implementation(libs.edc.transactionLocal) implementation(libs.tractus.sqlPool) - implementation(libs.postgres) + implementation(libs.apache.commonsLang) implementation(libs.flyway.core) + implementation(libs.postgres) + + implementation(libs.hikari) + testImplementation(libs.edc.junit) } diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/DataSourceFactory.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/DataSourceFactory.java new file mode 100644 index 000000000..91d6647bc --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/DataSourceFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import de.sovity.edc.extension.postgresql.utils.JdbcCredentials; +import lombok.RequiredArgsConstructor; + +import javax.sql.DataSource; + +@RequiredArgsConstructor +public class DataSourceFactory { + private final PostgresFlywayConfig config; + + + /** + * Create a new {@link DataSource} from EDC Config. + * + * @return {@link DataSource}. + */ + public HikariDataSource newDataSource() { + var jdbcCredentials = config.jdbcCredentials(); + int maxPoolSize = config.poolSize(); + int connectionTimeoutInMs = config.connectionTimeoutInMs(); + return newDataSource(jdbcCredentials, maxPoolSize, connectionTimeoutInMs); + } + + /** + * Create a new {@link DataSource}. + *
      + * This method is static, so we can use from test code. + * + * @param jdbcCredentials jdbc credentials + * @param maxPoolSize max pool size + * @param connectionTimeoutInMs connection timeout in ms + * @return {@link DataSource}. + */ + public static HikariDataSource newDataSource( + JdbcCredentials jdbcCredentials, + int maxPoolSize, + int connectionTimeoutInMs + ) { + var hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(jdbcCredentials.jdbcUrl()); + hikariConfig.setUsername(jdbcCredentials.jdbcUser()); + hikariConfig.setPassword(jdbcCredentials.jdbcPassword()); + hikariConfig.setMinimumIdle(1); + hikariConfig.setMaximumPoolSize(maxPoolSize); + hikariConfig.setIdleTimeout(30000); + hikariConfig.setPoolName("edc-server"); + hikariConfig.setMaxLifetime(50000); + hikariConfig.setConnectionTimeout(connectionTimeoutInMs); + + return new HikariDataSource(hikariConfig); + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java new file mode 100644 index 000000000..a742381be --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.flywaydb.core.Flyway; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.sql.DataSource; + +/** + * Quickly launch {@link Flyway} from EDC Config + */ +@RequiredArgsConstructor +public class FlywayFactory { + + private final PostgresFlywayConfig config; + + public Flyway setupFlywayForUnifiedHistory(DataSource dataSource) { + val locations = new ArrayList(); + locations.add("classpath:db/migration"); + locations.addAll(getAdditionalFlywayMigrationLocations()); + + return Flyway.configure() + .dataSource(dataSource) + .cleanDisabled(!config.flywayCleanEnabled()) + .table("flyway_schema_history") + .locations(locations.toArray(new String[0])) + .load(); + } + + public Flyway setupFlywayForUnifiedHistoryFromLegacyDatabase(DataSource dataSource) { + val locations = new ArrayList(); + locations.add("classpath:db/migration"); + locations.addAll(getAdditionalFlywayMigrationLocations()); + + return Flyway.configure() + .dataSource(dataSource) + .baselineVersion("8") + .cleanDisabled(!config.flywayCleanEnabled()) + .table("flyway_schema_history") + .locations(locations.toArray(new String[0])) + .load(); + } + + public List getAdditionalFlywayMigrationLocations() { + String commaJoined = config.edcFlywayAdditionalMigrationLocations(); + return Arrays.stream(commaJoined.split(",")) + .map(String::trim) + .filter(it -> !it.isEmpty()) + .toList(); + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayMigrator.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayMigrator.java new file mode 100644 index 000000000..03764ceba --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayMigrator.java @@ -0,0 +1,104 @@ +package de.sovity.edc.extension.postgresql; + +import com.zaxxer.hikari.HikariDataSource; +import de.sovity.edc.extension.postgresql.legacy.migration.DatabaseMigrationManager; +import de.sovity.edc.extension.postgresql.legacy.migration.FlywayService; +import de.sovity.edc.extension.postgresql.utils.DatabaseUtils; +import lombok.val; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.system.configuration.Config; +import org.flywaydb.core.api.FlywayException; + +import java.sql.SQLException; + +public class FlywayMigrator { + + private final PostgresFlywayConfig config; + private final Config legacyConfig; + private final HikariDataSource dataSource; + private final Monitor monitor; + private final FlywayFactory flywayFactory; + private final DatabaseUtils databaseUtils; + + public FlywayMigrator(PostgresFlywayConfig config, Config legacyConfig, HikariDataSource dataSource, Monitor monitor) { + this.config = config; + this.legacyConfig = legacyConfig; + this.dataSource = dataSource; + this.monitor = monitor; + this.flywayFactory = new FlywayFactory(config); + this.databaseUtils = new DatabaseUtils(dataSource); + } + + public void updateDatabaseWithLegacyHandling() { + migrateLegacyHistory(); + cleanDatabase(); + updateDatabase(); + } + + private void migrateLegacyHistory() { + if (config.flywayClean() && config.flywayCleanEnabled()) { + return; + } + try { + val isLegacyDatabase = databaseUtils.hasLegacyMigrations(); + if (!isLegacyDatabase) { + return; + } + } catch (SQLException e) { + throw migrationFailed(e); + } + + monitor.debug(() -> "Legacy-style flyway migration table detected. Upgrading to the latest legacy database history revision."); + val flywayService = new FlywayService( + monitor, + config.flywayRepair(), + config.flywayCleanEnabled(), + config.flywayClean() + ); + val migrationManager = new DatabaseMigrationManager(legacyConfig, flywayService); + migrationManager.migrateAllDataSources(); + + monitor.debug(() -> "Upgrading to the latest unified database history revision."); + val flyway = flywayFactory.setupFlywayForUnifiedHistoryFromLegacyDatabase(dataSource); + + try { + flyway.baseline(); + } catch (FlywayException e) { + throw migrationFailed(e); + } + } + + private void cleanDatabase() { + + boolean shouldClean = config.flywayClean(); + boolean canClean = config.flywayCleanEnabled(); + if (shouldClean) { + if (!canClean) { + throw new IllegalStateException("In order to clean the history both %s and %s must be set to true.".formatted( + PostgresFlywayExtension.FLYWAY_CLEAN, PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE + )); + } + monitor.info(() -> "Cleaning database before migrations, since %s=true and %s=true.".formatted( + PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, PostgresFlywayExtension.FLYWAY_CLEAN + )); + + val flyway = flywayFactory.setupFlywayForUnifiedHistory(dataSource); + flyway.clean(); + } + } + + private void updateDatabase() { + val flyway = flywayFactory.setupFlywayForUnifiedHistory(dataSource); + + try { + flyway.migrate(); + } catch (FlywayException e) { + throw migrationFailed(e); + } + } + + private EdcPersistenceException migrationFailed(Exception e) { + return new EdcPersistenceException("Flyway migration failed", e); + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayConfig.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayConfig.java new file mode 100644 index 000000000..2eedacd1e --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.postgresql; + +import de.sovity.edc.extension.postgresql.utils.JdbcCredentials; +import org.apache.commons.lang3.Validate; +import org.eclipse.edc.spi.system.configuration.Config; + +public record PostgresFlywayConfig( + JdbcCredentials jdbcCredentials, + boolean flywayRepair, + boolean flywayCleanEnabled, + boolean flywayClean, + String edcFlywayAdditionalMigrationLocations, + int poolSize, + int connectionTimeoutInMs) { + + public static PostgresFlywayConfig fromConfig(Config config) { + return new PostgresFlywayConfig( + new JdbcCredentials( + getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_URL), + getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_USER), + getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_PASSWORD) + ), + config.getBoolean(PostgresFlywayExtension.EDC_DATASOURCE_REPAIR_SETTING, false), + config.getBoolean(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, false), + config.getBoolean(PostgresFlywayExtension.FLYWAY_CLEAN, false), + config.getString(PostgresFlywayExtension.EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS, ""), + config.getInteger(PostgresFlywayExtension.DB_CONNECTION_POOL_SIZE, 3), + config.getInteger(PostgresFlywayExtension.DB_CONNECTION_TIMEOUT_IN_MS, 5000) + ); + } + + public static String getRequiredStringProperty(Config config, String name) { + String value = config.getString(name, ""); + Validate.notBlank(value, "EDC Property '%s' is required".formatted(name)); + return value; + } + +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java index e44b528b6..357892532 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java @@ -14,22 +14,78 @@ package de.sovity.edc.extension.postgresql; -import de.sovity.edc.extension.postgresql.migration.DatabaseMigrationManager; -import de.sovity.edc.extension.postgresql.migration.FlywayService; +import com.zaxxer.hikari.HikariDataSource; +import lombok.val; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; public class PostgresFlywayExtension implements ServiceExtension { + /** + * The JDBC URL. + */ + @Setting(required = true) + public static final String JDBC_URL = "edc.datasource.default.url"; - @Setting + /** + * The JDBC user. + */ + @Setting(required = true) + public static final String JDBC_USER = "edc.datasource.default.user"; + + /** + * The JDBC password. + */ + @Setting(required = true) + public static final String JDBC_PASSWORD = "edc.datasource.default.password"; + + /** + * Attempts to fix the history when a migration fails. Only supported in older migration scripts. + */ + @Setting(defaultValue = "false") public static final String EDC_DATASOURCE_REPAIR_SETTING = "edc.flyway.repair"; - @Setting - public static final String FLYWAY_CLEAN_ENABLED = "edc.flyway.clean.enable"; - @Setting + + /** + * Allows the deletion of the database. Goes in pair with {@link #FLYWAY_CLEAN}. Both options must be enabled for a clean to happen. + */ + @Setting(defaultValue = "false") + public static final String FLYWAY_CLEAN_ENABLE = "edc.flyway.clean.enable"; + + /** + * Request the deletion of the database. Goes in pair with {@link #FLYWAY_CLEAN_ENABLE}. Both options must be enabled for a clean to happen. + */ + @Setting(defaultValue = "false") public static final String FLYWAY_CLEAN = "edc.flyway.clean"; + /** + * Sets the connection pool size to use during the flyway migration. + */ + @Setting(defaultValue = "3") + public static final String DB_CONNECTION_POOL_SIZE = "edc.server.db.connection.pool.size"; + + /** + * Sets the connection timeout for the datasource in milliseconds. + */ + @Setting(defaultValue = "5000") + public static final String DB_CONNECTION_TIMEOUT_IN_MS = "edc.server.db.connection.timeout.in.ms"; + + /** + * Coma-separated list of additional migration scripts files. + *
      + * Additional Migration Scripts can be added by specifying the configuration property + * `edc.flyway.additional.migration.locations`. + *
      + * Values are comma separated and need to be correct FlyWay migration script locations. + * These migration scripts need to be compatible with the migrations in {@code resources/db/migration} and {@code resources/db/migration/legacy}. + *
      + */ + @Setting(defaultValue = "") + public static final String EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS = "edc.flyway.additional.migration.locations"; + + + private HikariDataSource dataSource; + @Override public String name() { return "Postgres Flyway Extension"; @@ -37,14 +93,14 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var flywayService = new FlywayService( - context.getMonitor(), - context.getSetting(EDC_DATASOURCE_REPAIR_SETTING, false), - context.getSetting(FLYWAY_CLEAN_ENABLED, false), - context.getSetting(FLYWAY_CLEAN, false) - ); - var migrationManager = new DatabaseMigrationManager(context.getConfig(), flywayService); - migrationManager.migrateAllDataSources(); + val legacyConfig = context.getConfig(); + val extensionConfig = PostgresFlywayConfig.fromConfig(legacyConfig); + + val dataSourceFactory = new DataSourceFactory(extensionConfig); + dataSource = dataSourceFactory.newDataSource(); + + val migrator = new FlywayMigrator(extensionConfig, legacyConfig, dataSource, context.getMonitor()); + migrator.updateDatabaseWithLegacyHandling(); } } diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/connection/DriverManagerConnectionFactory.java similarity index 95% rename from extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java rename to extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/connection/DriverManagerConnectionFactory.java index 64016df9b..13d2946e7 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/DriverManagerConnectionFactory.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/connection/DriverManagerConnectionFactory.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.extension.postgresql.connection; +package de.sovity.edc.extension.postgresql.legacy.connection; import org.eclipse.edc.spi.persistence.EdcPersistenceException; import org.eclipse.edc.sql.ConnectionFactory; diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/connection/JdbcConnectionProperties.java similarity index 96% rename from extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java rename to extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/connection/JdbcConnectionProperties.java index b8fcb8141..1ed3830c6 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/connection/JdbcConnectionProperties.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/connection/JdbcConnectionProperties.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.extension.postgresql.connection; +package de.sovity.edc.extension.postgresql.legacy.connection; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.configuration.Config; diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/migration/DatabaseMigrationManager.java similarity index 92% rename from extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java rename to extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/migration/DatabaseMigrationManager.java index 71794207d..659d4dee7 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/DatabaseMigrationManager.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/migration/DatabaseMigrationManager.java @@ -12,9 +12,9 @@ * */ -package de.sovity.edc.extension.postgresql.migration; +package de.sovity.edc.extension.postgresql.legacy.migration; -import de.sovity.edc.extension.postgresql.connection.JdbcConnectionProperties; +import de.sovity.edc.extension.postgresql.legacy.connection.JdbcConnectionProperties; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.configuration.Config; @@ -24,7 +24,6 @@ public class DatabaseMigrationManager { @Setting public static final String EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS = "edc.flyway.additional.migration.locations"; - private static final String EDC_DATASOURCE_PREFIX = "edc.datasource"; private static final String DEFAULT_DATASOURCE = "default"; private final Config config; diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/migration/FlywayService.java similarity index 95% rename from extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java rename to extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/migration/FlywayService.java index 6a7af5246..9fc9ec60c 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/migration/FlywayService.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/legacy/migration/FlywayService.java @@ -12,10 +12,10 @@ * */ -package de.sovity.edc.extension.postgresql.migration; +package de.sovity.edc.extension.postgresql.legacy.migration; -import de.sovity.edc.extension.postgresql.connection.DriverManagerConnectionFactory; -import de.sovity.edc.extension.postgresql.connection.JdbcConnectionProperties; +import de.sovity.edc.extension.postgresql.legacy.connection.DriverManagerConnectionFactory; +import de.sovity.edc.extension.postgresql.legacy.connection.JdbcConnectionProperties; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.persistence.EdcPersistenceException; @@ -33,7 +33,7 @@ @RequiredArgsConstructor public class FlywayService { - private static final String MIGRATION_LOCATION_BASE = "classpath:migration"; + private static final String MIGRATION_LOCATION_BASE = "classpath:db/legacy/migration"; private final Monitor monitor; private final boolean tryRepairOnFailedMigration; @@ -140,5 +140,4 @@ private void handleFlywayMigrationResult(String datasourceName, MigrateResult mi } } - } diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/DatabaseUtils.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/DatabaseUtils.java new file mode 100644 index 000000000..295555ae2 --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/DatabaseUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.postgresql.utils; + +import lombok.RequiredArgsConstructor; +import lombok.val; + +import java.sql.SQLException; +import javax.sql.DataSource; + +@RequiredArgsConstructor +public class DatabaseUtils { + + public static class TableNames { + public static final String UNIFIED_MIGRATION_TABLE = "flyway_schema_history"; + public static final String DEFAULT_MIGRATION_TABLE = "flyway_schema_history_default"; + } + + private final DataSource dataSource; + + public boolean tableExists(String tableName) throws SQLException { + try ( + val connection = dataSource.getConnection(); + val stmt = connection.prepareStatement( + "select exists (select 1 from information_schema.tables where table_schema = 'public' and table_name = ?)")) { + stmt.setString(1, tableName); + val result = stmt.executeQuery(); + if (!result.next()) { + return false; + } + return result.getBoolean("exists"); + } + } + + public boolean hasBaseline(String tableName, String baseline) throws SQLException { + try ( + val connection = dataSource.getConnection(); + val stmt = connection.prepareStatement( + "select exists (select 1 from public." + tableName + " where type = 'BASELINE' and version = ?)")) { + stmt.setString(1, baseline); + val result = stmt.executeQuery(); + if (!result.next()) { + return false; + } + return result.getBoolean("exists"); + } + } + + public boolean hasUnifiedMigrations() throws SQLException { + return tableExists(TableNames.UNIFIED_MIGRATION_TABLE); + } + + public boolean hasDefaultMigrations() throws SQLException { + return tableExists(TableNames.DEFAULT_MIGRATION_TABLE); + } + + public boolean hasSplitHistory() throws SQLException { + return tableExists("flyway_schema_history_asset") || + tableExists("flyway_schema_history_contractdefinition") || + tableExists("flyway_schema_history_contractnegotiation") || + tableExists("flyway_schema_history_dataplaneinstance") || + tableExists("flyway_schema_history_policy") || + tableExists("flyway_schema_history_transferprocess"); + } + + public boolean hasLegacyMigrations() throws SQLException { + return hasSplitHistory() || hasDefaultMigrations(); + } + + public boolean hasUnifiedBaseline(String s) throws SQLException { + return hasBaseline(TableNames.UNIFIED_MIGRATION_TABLE, s); + } +} diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/JdbcCredentials.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/JdbcCredentials.java new file mode 100644 index 000000000..c6c134d9a --- /dev/null +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/utils/JdbcCredentials.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql.utils; + +/** + * JDBC Credentials + * + * @param jdbcUrl JDBC URL without credentials + * @param jdbcUser JDBC User + * @param jdbcPassword JDBC Password + */ +public record JdbcCredentials( + String jdbcUrl, + String jdbcUser, + String jdbcPassword +) { +} diff --git a/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_1__Asset_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/asset/V0_0_1__Asset_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_1__Asset_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/asset/V0_0_1__Asset_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_2__Asset_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/asset/V0_0_2__Asset_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/asset/V0_0_2__Asset_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/asset/V0_0_2__Asset_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_1__ContractDefinition_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_2__ContractDefinition_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_3__ContractDefinition_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractdefinition/V0_0_4__Set_Default_Validity.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractnegotiation/V0_0_1__ContractNegotiation_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractnegotiation/V0_0_2__ContractNegotiation_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/contractnegotiation/V0_0_3__Fix_Contract_Offer_JSON.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/dataplaneinstance/V0_0_1__DataplaneInstance_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V1_0_0_Example_Migration.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V1_0_0_Example_Migration.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/default/V1_0_0_Example_Migration.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V1_0_0_Example_Migration.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V2__Delete-Transfer-Processes-Trigger.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V2__Delete-Transfer-Processes-Trigger.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/default/V2__Delete-Transfer-Processes-Trigger.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V2__Delete-Transfer-Processes-Trigger.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V3__MS8-to-0.2.1.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V3__MS8-to-0.2.1.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/default/V3__MS8-to-0.2.1.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V3__MS8-to-0.2.1.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V4__MS8-to-0.2.1_bugfixes.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V4__MS8-to-0.2.1_bugfixes.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/default/V4__MS8-to-0.2.1_bugfixes.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V4__MS8-to-0.2.1_bugfixes.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V5__Mobility_DCAT_Mapping.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V5__Mobility_DCAT_Mapping.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/default/V5__Mobility_DCAT_Mapping.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V5__Mobility_DCAT_Mapping.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/default/V6__Fix_DataModel_ID_Field.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V6__Fix_DataModel_ID_Field.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/default/V6__Fix_DataModel_ID_Field.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V6__Fix_DataModel_ID_Field.sql diff --git a/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V7__last_legacy_migration_do_not_continue_here.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V7__last_legacy_migration_do_not_continue_here.sql new file mode 100644 index 000000000..f86d034a1 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/default/V7__last_legacy_migration_do_not_continue_here.sql @@ -0,0 +1,3 @@ +-- Do not add any more migration in this folder structure. +-- These are legacy migrations and any new migration must be placed in the `db/migration` folder. +-- See the README in `resources/db`. diff --git a/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_1__Policy_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/policy/V0_0_1__Policy_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_1__Policy_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/policy/V0_0_1__Policy_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_2__Policy_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/policy/V0_0_2__Policy_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/policy/V0_0_2__Policy_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/policy/V0_0_2__Policy_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_1__TransferProcess_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_2__TransferProcess_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_3__TransferProcess_Schema.sql diff --git a/extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_4__Set_Default_Properties.sql b/extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_4__Set_Default_Properties.sql similarity index 100% rename from extensions/postgres-flyway/src/main/resources/migration/transferprocess/V0_0_4__Set_Default_Properties.sql rename to extensions/postgres-flyway/src/main/resources/db/legacy/migration/transferprocess/V0_0_4__Set_Default_Properties.sql diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V1__MS8.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V1__MS8.sql new file mode 100644 index 000000000..e916c5e30 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V1__MS8.sql @@ -0,0 +1,505 @@ +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - initial API and implementation for DataplaneInstances +-- +-- +CREATE TABLE IF NOT EXISTS edc_data_plane_instance +( + id VARCHAR NOT NULL, + data JSON NOT NULL, + PRIMARY KEY (id) +); +-- Statements are designed for and tested with Postgres only! + +CREATE TABLE IF NOT EXISTS edc_lease +( + leased_by VARCHAR NOT NULL, + leased_at BIGINT, + lease_duration INTEGER NOT NULL, + lease_id VARCHAR NOT NULL + CONSTRAINT lease_pk + PRIMARY KEY +); + +COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; + +COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; + +CREATE TABLE IF NOT EXISTS edc_transfer_process +( + transferprocess_id VARCHAR NOT NULL + CONSTRAINT transfer_process_pk + PRIMARY KEY, + type VARCHAR NOT NULL, + state INTEGER NOT NULL, + state_count INTEGER DEFAULT 0 NOT NULL, + state_time_stamp BIGINT, + created_time_stamp BIGINT, + trace_context JSON, + error_detail VARCHAR, + resource_manifest JSON, + provisioned_resource_set JSON, + content_data_address JSON, + deprovisioned_resources JSON, + lease_id VARCHAR + CONSTRAINT transfer_process_lease_lease_id_fk + REFERENCES edc_lease + ON DELETE SET NULL +); + +COMMENT ON COLUMN edc_transfer_process.trace_context IS 'Java Map serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.resource_manifest IS 'java ResourceManifest serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.provisioned_resource_set IS 'ProvisionedResourceSet serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.content_data_address IS 'DataAddress serialized as JSON'; + +COMMENT ON COLUMN edc_transfer_process.deprovisioned_resources IS 'List of deprovisioned resources, serialized as JSON'; + + +CREATE UNIQUE INDEX IF NOT EXISTS transfer_process_id_uindex + ON edc_transfer_process (transferprocess_id); + +CREATE TABLE IF NOT EXISTS edc_data_request +( + datarequest_id VARCHAR NOT NULL + CONSTRAINT data_request_pk + PRIMARY KEY, + process_id VARCHAR NOT NULL, + connector_address VARCHAR NOT NULL, + protocol VARCHAR NOT NULL, + connector_id VARCHAR, + asset_id VARCHAR NOT NULL, + contract_id VARCHAR NOT NULL, + data_destination JSON NOT NULL, + managed_resources BOOLEAN DEFAULT TRUE, + properties JSON, + transfer_type JSON, + transfer_process_id VARCHAR NOT NULL + CONSTRAINT data_request_transfer_process_id_fk + REFERENCES edc_transfer_process + ON UPDATE RESTRICT ON DELETE CASCADE +); + +COMMENT ON COLUMN edc_data_request.data_destination IS 'DataAddress serialized as JSON'; + +COMMENT ON COLUMN edc_data_request.properties IS 'java Map serialized as JSON'; + +COMMENT ON COLUMN edc_data_request.transfer_type IS 'TransferType serialized as JSON'; + + +CREATE UNIQUE INDEX IF NOT EXISTS data_request_id_uindex + ON edc_data_request (datarequest_id); + +CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex + ON edc_lease (lease_id); + +-- Empty example migration +-- This directory will contain all our Community Edition migrations in the future. +-- Commercial Edition migrations will have to be compatible version-wise. +-- This file is added to prevent flyway from crashing due no migrations. +SELECT 1; +-- Statements are designed for and tested with Postgres only! + +CREATE TABLE IF NOT EXISTS edc_lease +( + leased_by VARCHAR NOT NULL, + leased_at BIGINT, + lease_duration INTEGER DEFAULT 60000 NOT NULL, + lease_id VARCHAR NOT NULL + CONSTRAINT lease_pk + PRIMARY KEY +); + +COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; + +COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; + + +CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex + ON edc_lease (lease_id); + + + +CREATE TABLE IF NOT EXISTS edc_contract_agreement +( + agr_id VARCHAR NOT NULL + CONSTRAINT contract_agreement_pk + PRIMARY KEY, + provider_agent_id VARCHAR, + consumer_agent_id VARCHAR, + signing_date BIGINT, + start_date BIGINT, + end_date INTEGER, + asset_id VARCHAR NOT NULL, + policy JSON +); + + +CREATE TABLE IF NOT EXISTS edc_contract_negotiation +( + id VARCHAR NOT NULL + CONSTRAINT contract_negotiation_pk + PRIMARY KEY, + correlation_id VARCHAR, + counterparty_id VARCHAR NOT NULL, + counterparty_address VARCHAR NOT NULL, + protocol VARCHAR DEFAULT 'ids-multipart'::CHARACTER VARYING NOT NULL, + type INTEGER DEFAULT 0 NOT NULL, + state INTEGER DEFAULT 0 NOT NULL, + state_count INTEGER DEFAULT 0, + state_timestamp BIGINT, + error_detail VARCHAR, + agreement_id VARCHAR + CONSTRAINT contract_negotiation_contract_agreement_id_fk + REFERENCES edc_contract_agreement, + contract_offers JSON, + trace_context JSON, + lease_id VARCHAR + CONSTRAINT contract_negotiation_lease_lease_id_fk + REFERENCES edc_lease + ON DELETE SET NULL, + CONSTRAINT provider_correlation_id CHECK (type = '0' OR correlation_id IS NOT NULL) +); + +COMMENT ON COLUMN edc_contract_negotiation.agreement_id IS 'ContractAgreement serialized as JSON'; + +COMMENT ON COLUMN edc_contract_negotiation.contract_offers IS 'List serialized as JSON'; + +COMMENT ON COLUMN edc_contract_negotiation.trace_context IS 'Map serialized as JSON'; + + +CREATE INDEX IF NOT EXISTS contract_negotiation_correlationid_index + ON edc_contract_negotiation (correlation_id); + +CREATE UNIQUE INDEX IF NOT EXISTS contract_negotiation_id_uindex + ON edc_contract_negotiation (id); + +CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex + ON edc_contract_agreement (agr_id);-- + +-- +-- Copyright (c) 2022 Daimler TSS GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Daimler TSS GmbH - Initial SQL Query +-- Microsoft Corporation - refactoring +-- + +-- table: edc_contract_definitions +-- only intended for and tested with H2 and Postgres! +CREATE TABLE IF NOT EXISTS edc_contract_definitions +( + contract_definition_id VARCHAR NOT NULL, + access_policy_id VARCHAR NOT NULL, + contract_policy_id VARCHAR NOT NULL, + selector_expression JSON NOT NULL, + PRIMARY KEY (contract_definition_id) +); +-- +-- Copyright (c) 2022 ZF Friedrichshafen AG +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- ZF Friedrichshafen AG - Initial SQL Query +-- + +-- Statements are designed for and tested with Postgres only! + +-- table: edc_policydefinitions +CREATE TABLE IF NOT EXISTS edc_policydefinitions +( + policy_id VARCHAR NOT NULL, + permissions JSON, + prohibitions JSON, + duties JSON, + extensible_properties JSON, + inherits_from VARCHAR, + assigner VARCHAR, + assignee VARCHAR, + target VARCHAR, + policy_type VARCHAR NOT NULL, + PRIMARY KEY (policy_id) +); + +COMMENT ON COLUMN edc_policydefinitions.permissions IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.prohibitions IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.duties IS 'Java List serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.extensible_properties IS 'Java Map serialized as JSON'; +COMMENT ON COLUMN edc_policydefinitions.policy_type IS 'Java PolicyType serialized as JSON'; + +CREATE UNIQUE INDEX IF NOT EXISTS edc_policydefinitions_id_uindex + ON edc_policydefinitions (policy_id); +-- +-- Copyright (c) 2022 Daimler TSS GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Daimler TSS GmbH - Initial SQL Query +-- + +-- THIS SCHEMA HAS BEEN WRITTEN AND TESTED ONLY FOR POSTGRES + +-- table: edc_asset +CREATE TABLE IF NOT EXISTS edc_asset +( + asset_id VARCHAR NOT NULL, + PRIMARY KEY (asset_id) +); + +-- table: edc_asset_dataaddress +CREATE TABLE IF NOT EXISTS edc_asset_dataaddress +( + asset_id_fk VARCHAR NOT NULL, + properties JSON NOT NULL, + PRIMARY KEY (asset_id_fk), + FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE +); +COMMENT ON COLUMN edc_asset_dataaddress.properties IS 'DataAddress properties serialized as JSON'; + +-- table: edc_asset_property +CREATE TABLE IF NOT EXISTS edc_asset_property +( + asset_id_fk VARCHAR NOT NULL, + property_name VARCHAR NOT NULL, + property_value VARCHAR NOT NULL, + property_type VARCHAR NOT NULL, + PRIMARY KEY (asset_id_fk, property_name), + FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE +); + +COMMENT ON COLUMN edc_asset_property.property_name IS + 'Asset property key'; +COMMENT ON COLUMN edc_asset_property.property_value IS + 'Asset property value'; +COMMENT ON COLUMN edc_asset_property.property_type IS + 'Asset property class name'; + +CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value + ON edc_asset_property (property_name, property_value);-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_negotiation + ADD created_at BIGINT, + ADD updated_at BIGINT; + +UPDATE edc_contract_negotiation SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; +UPDATE edc_contract_negotiation SET updated_at=created_at; + +ALTER TABLE edc_contract_negotiation + ALTER COLUMN created_at SET NOT NULL; +ALTER TABLE edc_contract_negotiation + ALTER COLUMN updated_at SET NOT NULL; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_transfer_process + RENAME COLUMN created_time_stamp TO created_at; + +UPDATE edc_transfer_process + SET created_at = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 + WHERE created_at = NULL; + +ALTER TABLE edc_transfer_process + ADD updated_at BIGINT; + +UPDATE edc_transfer_process SET updated_at=created_at; + +ALTER TABLE edc_transfer_process ALTER COLUMN updated_at SET NOT NULL; +ALTER TABLE edc_transfer_process ALTER COLUMN created_at SET NOT NULL; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_contract_definitions + ADD created_at BIGINT; + +UPDATE edc_contract_definitions SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_contract_definitions + ALTER COLUMN created_at SET NOT NULL;-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- +ALTER TABLE edc_policydefinitions + ADD created_at BIGINT; + +UPDATE edc_policydefinitions SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_policydefinitions + ALTER COLUMN created_at SET NOT NULL; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +ALTER TABLE edc_asset + ADD created_at BIGINT; + +UPDATE edc_asset SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; + +ALTER TABLE edc_asset + ALTER COLUMN created_at SET NOT NULL;-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-7 EDC +-- +-- + +UPDATE edc_contract_negotiation +SET contract_offers = co.contract_offers_edited +FROM ( + SELECT + cn.id, + jsonb_agg( + jsonb_set( + jsonb_set( + elems, + '{contractStart}', + to_json(to_char(to_timestamp(created_at/1000) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ), + '{contractEnd}', + to_json(to_char(to_timestamp((created_at/1000) + 60 * 60 * 24 * 365) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb + ) + )::json as contract_offers_edited + FROM + edc_contract_negotiation cn, + jsonb_array_elements(cn.contract_offers::jsonb) elems + GROUP BY cn.id +) co +WHERE edc_contract_negotiation.id = co.id; + +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +ALTER TABLE edc_transfer_process ADD transferprocess_properties JSON; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +ALTER TABLE edc_contract_definitions ADD validity BIGINT;-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_transfer_process SET transferprocess_properties = '{}'::json WHERE transferprocess_properties IS NULL; +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables to Milestone-8 EDC +-- +-- +UPDATE edc_contract_definitions SET validity=60*60*24*365 WHERE validity IS NULL; diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V2__Delete-Transfer-Processes-Trigger.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V2__Delete-Transfer-Processes-Trigger.sql new file mode 100644 index 000000000..5ca84691b --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V2__Delete-Transfer-Processes-Trigger.sql @@ -0,0 +1,41 @@ +-- Required for reasonably fast ON DELETE CASCADE from edc_transfer_process +create index data_request_transfer_process_id_idx + on edc_data_request (transfer_process_id); +-- Speed up sort + limit query +-- Include transferprocess_id to enable index-only scan +create index transfer_process_created_at_idx + on edc_transfer_process (created_at) include (transferprocess_id); + +-- Delete oldest row when table size exceeds 3000 rows +-- The row count should mostly stabilize slightly above 3000, as the reltuples data in pg_class is only updated by VACUUM +-- Unfortunately, I was not able to get conclusive results on the behavior under concurrent inserts +-- One problem is that the table might still grow over time, if concurrent inserts can delete the same row +-- To avoid this, we could delete two rows instead of just one +-- Then the table would shrink until the next auto-vacuum detects that it is below 3000 rows again +create function transfer_process_delete_old_rows() returns trigger as $$ +begin + delete from edc_transfer_process o + using ( + select i2.transferprocess_id + from edc_transfer_process i2 + order by i2.created_at + limit 2 + ) i, + ( + -- Hack to avoid count(*), which takes several hundred milliseconds + -- Not perfectly accurate, but close enough + -- Idea taken from: https://www.cybertec-postgresql.com/en/postgresql-count-made-fast/ + select pgc.reltuples::bigint as count + from pg_catalog.pg_class pgc + where pgc.relname = 'edc_transfer_process' + ) c + where i.transferprocess_id = o.transferprocess_id and c.count > 3000; + + return null; +end; +$$ language plpgsql; + +create trigger delete_old_rows after insert + on edc_transfer_process + for each row +execute function transfer_process_delete_old_rows(); \ No newline at end of file diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V3__MS8-to-0.2.1.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V3__MS8-to-0.2.1.sql new file mode 100644 index 000000000..e08d4659c --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V3__MS8-to-0.2.1.sql @@ -0,0 +1,416 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables From MS8 to 0.1.0 EDC +-- +-- + +-- Migrates an Asset ID +create + or replace function pg_temp.migrate_asset_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:artifact:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Migrate a contract agreement ID to EDC 0 +create + or replace function pg_temp.migrate_contract_agreement_id(contract_agreement_id text, asset_id text) returns text as +$$ +begin + return pg_temp.base64encode(split_part(contract_agreement_id, ':', 1)) || ':' || + pg_temp.base64encode(asset_id) || ':' || + pg_temp.base64encode(split_part(contract_agreement_id, ':', 2)); +end; +$$ + language plpgsql; + +-- Migrates a Connector Endpoint to EDC 0 +create + or replace function pg_temp.migrate_connector_endpoint(endpoint text) returns text as +$$ +begin + return pg_temp.replace_suffix(endpoint, '/api/v1/ids/data', '/api/dsp'); +end; +$$ + language plpgsql; + +-- Migrates a Participant ID to EDC 0 +create + or replace function pg_temp.migrate_participant_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:connector:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Migrates an Asset Property Name to EDC 0 (if possible) +create + or replace function pg_temp.migrate_asset_property_name(asset_property_key text) returns text as +$$ +begin + return case asset_property_key + -- This list only contains properties that are directly mappable + -- Properties that require a new nested JSON structure are not included + when 'asset:prop:id' then 'https://w3id.org/edc/v0.0.1/ns/id' + when 'asset:prop:name' then 'http://purl.org/dc/terms/title' + when 'asset:prop:language' then 'http://purl.org/dc/terms/language' + when 'asset:prop:description' then 'http://purl.org/dc/terms/description' + when 'asset:prop:standardLicense' then 'http://purl.org/dc/terms/license' + when 'asset:prop:version' then 'http://www.w3.org/ns/dcat#version' + when 'asset:prop:keywords' then 'http://www.w3.org/ns/dcat#keyword' + when 'asset:prop:contenttype' then 'http://www.w3.org/ns/dcat#mediaType' + when 'asset:prop:endpointDocumentation' then 'http://www.w3.org/ns/dcat#landingPage' + when 'http://w3id.org/mds#dataCategory' then asset_property_key + when 'http://w3id.org/mds#dataSubcategory' then asset_property_key + when 'http://w3id.org/mds#dataModel' then asset_property_key + when 'http://w3id.org/mds#geoReferenceMethod' then asset_property_key + when 'http://w3id.org/mds#transportMode' then asset_property_key + when 'asset:prop:datasource:http:hints:proxyMethod' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod' + when 'asset:prop:datasource:http:hints:proxyPath' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath' + when 'asset:prop:datasource:http:hints:proxyQueryParams' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams' + when 'asset:prop:datasource:http:hints:proxyBody' + then 'https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody' + else pg_temp.migrate_unknown_asset_property_name(asset_property_key) + end; +end; +$$ + language plpgsql; + +-- Migrates an unknown Asset Property Name to EDC 0 (if possible) +create + or replace function pg_temp.migrate_unknown_asset_property_name(asset_property_key text) returns text as +$$ +begin + asset_property_key := replace(asset_property_key, 'asset:prop:', ''); + if pg_temp.starts_with(asset_property_key, 'http://') or + pg_temp.starts_with(asset_property_key, 'https://') then + return asset_property_key; + end if; + + return 'http://unknown/' || replace(asset_property_key, ':', '-'); +end; +$$ + language plpgsql; + + +-- Migrates the "keywords" property value to EDC 0 +-- 'a, b'::text becomes '["a", "b"]'::text +create + or replace function pg_temp.migrate_asset_keywords(keywords_comma_joined text) returns text as +$$ +begin + return (select coalesce(json_agg(to_json(trim(keyword)))::text, '[]') + from unnest(regexp_split_to_array(keywords_comma_joined, ',')) as keyword + where trim(keyword) <> ''); +end; +$$ + language plpgsql; + +-- Migrates an asset property to EDC 0 +create + or replace function pg_temp.migrate_asset_property(property_name text, property_value text, property_type text) returns jsonb as +$$ +declare + name_mapped text; + value_mapped text; + type_mapped text; +begin + if property_name = 'asset:prop:id' then + name_mapped = pg_temp.migrate_asset_property_name(property_name); + value_mapped = pg_temp.migrate_asset_id(property_value); + type_mapped = property_type; + elsif property_name = 'asset:prop:keywords' then + name_mapped = pg_temp.migrate_asset_property_name(property_name); + value_mapped = pg_temp.migrate_asset_keywords(property_value); + type_mapped = 'java.util.ArrayList'; + elsif property_name = 'asset:prop:publisher' then + name_mapped = 'http://purl.org/dc/terms/publisher'; + value_mapped = jsonb_build_object('http://xmlns.com/foaf/0.1/homepage', + pg_temp.jsonld_value(property_value))::text; + type_mapped = 'java.util.LinkedHashMap'; + elsif property_name = 'asset:prop:curatorOrganizationName' or + property_name = 'asset:prop:originatorOrganization' then + name_mapped = 'http://purl.org/dc/terms/creator'; + value_mapped = jsonb_build_object('http://xmlns.com/foaf/0.1/name', + pg_temp.jsonld_value(property_value))::text; + type_mapped = 'java.util.LinkedHashMap'; + else + name_mapped = pg_temp.migrate_asset_property_name(property_name); + value_mapped = property_value; + type_mapped = property_type; + end if; + + return jsonb_build_object( + 'name', name_mapped, + 'value', value_mapped, + 'type', type_mapped + ); +end; +$$ + language plpgsql; + +-- Migrates a contract definition criterion operator +create + or replace function pg_temp.migrate_criterion_operator(op text) returns text as +$$ +begin + -- due to previous mixing of the criterion operator with ODRL operators, we need to ensure the data in the db + -- is correct + return case lower(op) + when 'eq' then '=' + when 'in' then 'in' + when 'like' then 'like' + else op + end; +end; +$$ + language plpgsql; + +-- Migrates a contract definition criterion to EDC 0 +create + or replace function pg_temp.migrate_criterion(criterion jsonb) returns jsonb as +$$ +declare + operand_left_mapped text; + operator_mapped text; + operand_right_mapped jsonb; +begin + operand_left_mapped = pg_temp.migrate_asset_property_name(criterion ->> 'operandLeft'); + operator_mapped = pg_temp.migrate_criterion_operator(criterion ->> 'operator'); + + if criterion ->> 'operandLeft' = 'asset:prop:id' then + if jsonb_typeof(criterion -> 'operandRight') = 'array' then + operand_right_mapped = (select jsonb_agg(to_jsonb(pg_temp.migrate_asset_id(items.item))) + from (select jsonb_array_elements_text(criterion -> 'operandRight') item) items); + else + operand_right_mapped = to_jsonb(pg_temp.migrate_asset_id(criterion ->> 'operandRight')); + end if; + else + operand_right_mapped = criterion -> 'operandRight'; + end if; + + return jsonb_build_object( + 'operandLeft', operand_left_mapped, + 'operator', operator_mapped, + 'operandRight', operand_right_mapped + ); +end; +$$ + language plpgsql; + +-- Migrates a contract offer JSON to EDC 0 +create + or replace function pg_temp.migrate_negotiation_contract_offer(contract_offer jsonb) returns jsonb as +$$ +begin + return (contract_offer - '{asset,provider,consumer,offerStart,offerEnd,contractStart,contractEnd}'::text[] + || jsonb_build_object('assetId', pg_temp.migrate_asset_id(contract_offer -> 'asset' ->> 'id'))); +end; +$$ + language plpgsql; + +-- Migrates a JSON array of contract offers to EDC 0 +create + or replace function pg_temp.migrate_negotiation_contract_offers(contract_offers jsonb) returns jsonb as +$$ +begin + return (select jsonb_agg(pg_temp.migrate_negotiation_contract_offer(contract_offers.contract_offer)) + from (select jsonb_array_elements(contract_offers) contract_offer) contract_offers); +end; +$$ + language plpgsql; + +-- Utility Function: Wraps a value in expanded JSON-LD +-- 'a'::text becomes '[{"@value": "a"}]'::jsonb +create + or replace function pg_temp.jsonld_value(value text) returns jsonb as +$$ +begin + return jsonb_build_array(jsonb_build_object('@value', to_jsonb(value))); +end; +$$ + language plpgsql; + +-- Utility Function: base64 encode +create + or replace function pg_temp.base64encode(str text) returns text as +$$ +begin + return encode(str::bytea, 'base64'); +end; +$$ + language plpgsql; + +-- Utility Function: replaceSuffix +create + or replace function pg_temp.replace_suffix(str text, old_suffix text, new_suffix text) returns text as +$$ +begin + return case + when pg_temp.ends_with(str, old_suffix) then + left(str, length(str) - length(old_suffix)) || new_suffix + else + str + end; +end; +$$ + language plpgsql; + +-- Utility Function: endsWith +create or replace function pg_temp.ends_with(str text, suffix text) + returns boolean as +$$ +begin + return right(str, length(suffix)) = suffix; +end; +$$ language plpgsql; + +-- Utility Function: startsWith +create or replace function pg_temp.starts_with(str text, prefix text) + returns boolean as +$$ +begin + return left(str, length(prefix)) = prefix; +end; +$$ language plpgsql; + +-- Asset IDs +alter table edc_asset_dataaddress + drop constraint edc_asset_dataaddress_asset_id_fk_fkey; +alter table edc_asset_property + drop constraint edc_asset_property_asset_id_fk_fkey; +update edc_asset +set asset_id = pg_temp.migrate_asset_id(asset_id); +update edc_asset_dataaddress +set asset_id_fk = pg_temp.migrate_asset_id(asset_id_fk); +update edc_asset_property +set asset_id_fk = pg_temp.migrate_asset_id(asset_id_fk); +update edc_contract_agreement +set asset_id = pg_temp.migrate_asset_id(asset_id); +update edc_data_request +set asset_id = pg_temp.migrate_asset_id(asset_id); +alter table edc_asset_dataaddress + add constraint edc_asset_dataaddress_asset_id_fk_fkey foreign key (asset_id_fk) references edc_asset (asset_id) on delete cascade; +alter table edc_asset_property + add constraint edc_asset_property_asset_id_fk_fkey foreign key (asset_id_fk) references edc_asset (asset_id) on delete cascade; + +-- Contract Agreement IDs +alter table edc_contract_negotiation + drop constraint contract_negotiation_contract_agreement_id_fk; +update edc_contract_negotiation +set agreement_id = pg_temp.migrate_contract_agreement_id(agreement_id, + pg_temp.migrate_asset_id(contract_offers -> 0 -> 'asset' ->> 'id')); +update edc_contract_agreement +set agr_id = pg_temp.migrate_contract_agreement_id(agr_id, asset_id); +update edc_data_request +set contract_id = pg_temp.migrate_contract_agreement_id(contract_id, asset_id); +alter table edc_contract_negotiation + add constraint contract_negotiation_contract_agreement_id_fk foreign key (agreement_id) references edc_contract_agreement (agr_id); + +-- Protocol +update edc_contract_negotiation +set protocol = 'dataspace-protocol-http'; + +-- Connector Endpoints +update edc_contract_negotiation +set counterparty_address = pg_temp.migrate_connector_endpoint(counterparty_address); +update edc_data_request +set connector_address = pg_temp.migrate_connector_endpoint(connector_address); + + +-- Participant IDs +update edc_data_request +set connector_id = pg_temp.migrate_participant_id(connector_id); +update edc_contract_agreement +set provider_agent_id = pg_temp.migrate_participant_id(provider_agent_id), + consumer_agent_id = pg_temp.migrate_participant_id(consumer_agent_id); + +-- Asset Properties +alter table edc_asset_property + add column property_is_private boolean; +delete +from edc_asset_property legacy_prop +where legacy_prop.property_name = 'asset:prop:originator'; +delete +from edc_asset_property legacy_prop +where legacy_prop.property_name = 'asset:prop:originatorOrganization' + and exists(select 1 + from edc_asset_property newer_prop + where legacy_prop.asset_id_fk = newer_prop.asset_id_fk + and newer_prop.property_name = 'asset:prop:curatorOrganizationName'); -- prevents errors from merging the legacy properties +update edc_asset_property +set property_name = pg_temp.migrate_asset_property(property_name, property_value, property_type) ->> 'name', + property_value = pg_temp.migrate_asset_property(property_name, property_value, property_type) ->> 'value', + property_type = pg_temp.migrate_asset_property(property_name, property_value, property_type) ->> 'type'; + +-- Contract Negotiation Type +alter table edc_contract_negotiation + drop constraint provider_correlation_id; +alter table edc_contract_negotiation + add column "type_new" text; +update edc_contract_negotiation +set "type_new" = case "type" when 1 then 'PROVIDER' else 'CONSUMER' end; +alter table edc_contract_negotiation + drop column "type"; +alter table edc_contract_negotiation + rename column "type_new" to "type"; +alter table edc_contract_negotiation + add constraint provider_correlation_id check (type = 'CONSUMER' OR correlation_id IS NOT NULL); + +-- Contract Negotiation Contract Offers +update edc_contract_negotiation +set contract_offers = pg_temp.migrate_negotiation_contract_offers(contract_offers::jsonb)::json; + +-- Contract Definitions Asset Selector +alter table edc_contract_definitions + rename column selector_expression to assets_selector; +with cd_updated as (select cd.contract_definition_id, + jsonb_agg(pg_temp.migrate_criterion(criterion))::json as asset_selector + from edc_contract_definitions cd, + jsonb_array_elements(cd.assets_selector::jsonb -> 'criteria') criterion + group by cd.contract_definition_id) +update edc_contract_definitions cd +set assets_selector = cd_updated.asset_selector +from cd_updated +where cd_updated.contract_definition_id = cd.contract_definition_id; + +-- Fix transfer processes stuck in running state +update edc_transfer_process +set state = 800 +where state = 600; + +-- Other DDL Changes +alter table edc_contract_negotiation + add column callback_addresses json; +alter table edc_contract_negotiation + add column pending boolean default false; +alter table edc_transfer_process + add column pending boolean default false; +alter table edc_transfer_process + add column callback_addresses json; + +alter table edc_transfer_process + rename column transferprocess_properties to private_properties; + +alter table edc_contract_definitions + drop column validity; +alter table edc_data_request + drop column if exists managed_resources; +alter table edc_data_request + drop column if exists properties; +alter table edc_data_request + drop column if exists transfer_type; diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V4__MS8-to-0.2.1_bugfixes.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V4__MS8-to-0.2.1_bugfixes.sql new file mode 100644 index 000000000..ee64152ba --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V4__MS8-to-0.2.1_bugfixes.sql @@ -0,0 +1,48 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Update Tables From MS8 to 0.1.0 EDC +-- +-- + +-- Migrates a Participant ID to EDC 0 +create + or replace function pg_temp.migrate_participant_id(asset_id text) returns text as +$$ +begin + return replace(replace(asset_id::text, 'urn:connector:', ''), ':', '-'); +end; +$$ + language plpgsql; + +-- Participant IDs +update edc_contract_negotiation +set counterparty_id = pg_temp.migrate_participant_id(counterparty_id); + +update edc_contract_agreement +set provider_agent_id = neg.counterparty_id +from edc_contract_negotiation neg +where neg.agreement_id = edc_contract_agreement.agr_id + and neg.type = 'CONSUMER'; + +update edc_contract_agreement +set consumer_agent_id = neg.counterparty_id +from edc_contract_negotiation neg +where neg.agreement_id = edc_contract_agreement.agr_id + and neg.type = 'PROVIDER'; + +-- Optimizations for Transfer Processes +create index transfer_process_status + on edc_transfer_process (state); + +-- Fix transfer processes stuck in running state +update edc_transfer_process +set state = 800 +where state = 600; diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V5__Mobility_DCAT_Mapping.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V5__Mobility_DCAT_Mapping.sql new file mode 100644 index 000000000..64a9ed66c --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V5__Mobility_DCAT_Mapping.sql @@ -0,0 +1,76 @@ +-- +-- Copyright (c) 2023 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - SQL Script +-- +-- + + +-- "http://www.w3.org/ns/dcat#distribution": { +-- "http://www.w3.org/ns/dcat#mediaType": "previously http://www.w3.org/ns/dcat#mediaType", +-- "https://w3id.org/mobilitydcat-ap/mobilityDataStandard": { +-- "@id": "previously http://w3id.org/mds#dataModel" +-- } +-- }, +with data as ( + select asset_id, + (select property_value from edc_asset_property p where p.property_name = 'http://www.w3.org/ns/dcat#mediaType' and p.asset_id_fk = a.asset_id) as media_type, + (select property_value from edc_asset_property p where p.property_name = 'http://w3id.org/mds#dataModel' and p.asset_id_fk = a.asset_id) as data_model + from edc_asset a +) +insert into edc_asset_property (asset_id_fk, property_name, property_type, property_value) ( + select asset_id, + 'http://www.w3.org/ns/dcat#distribution', + 'java.util.LinkedHashMap', + jsonb_build_object( + 'http://www.w3.org/ns/dcat#mediaType', media_type, + 'https://w3id.org/mobilitydcat-ap/mobilityDataStandard', jsonb_build_object('@id', data_model) + )::text + from data + where media_type is not null or data_model is not null +); +delete from edc_asset_property where property_name = 'http://www.w3.org/ns/dcat#mediaType'; +delete from edc_asset_property where property_name = 'http://w3id.org/mds#dataModel'; + + +-- "https://w3id.org/mobilitydcat-ap/mobilityTheme": { +-- "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category": "previously http://w3id.org/mds#dataCategory", +-- "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category": "previously http://w3id.org/mds#dataSubcategory" +-- }, +with data as ( + select asset_id, + (select property_value from edc_asset_property p where p.property_name = 'http://w3id.org/mds#dataCategory' and p.asset_id_fk = a.asset_id) as data_category, + (select property_value from edc_asset_property p where p.property_name = 'http://w3id.org/mds#dataSubcategory' and p.asset_id_fk = a.asset_id) as data_subcategory + from edc_asset a +) +insert into edc_asset_property (asset_id_fk, property_name, property_type, property_value) ( + select asset_id, + 'https://w3id.org/mobilitydcat-ap/mobilityTheme', + 'java.util.LinkedHashMap', + jsonb_build_object( + 'https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category', data_category, + 'https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category', data_subcategory + )::text + from data + where data_category is not null or data_subcategory is not null +); +delete from edc_asset_property where property_name = 'http://w3id.org/mds#dataCategory'; +delete from edc_asset_property where property_name = 'http://w3id.org/mds#dataSubcategory'; + + +-- "https://w3id.org/mobilitydcat-ap/georeferencingMethod": "previously http://w3id.org/mds#geoReferenceMethod", +update edc_asset_property +set property_name = 'https://w3id.org/mobilitydcat-ap/georeferencingMethod' +where property_name = 'http://w3id.org/mds#geoReferenceMethod'; + +-- "https://w3id.org/mobilitydcat-ap/transportMode": "previously http://w3id.org/mds#transportMode" +update edc_asset_property +set property_name = 'https://w3id.org/mobilitydcat-ap/transportMode' +where property_name = 'http://w3id.org/mds#transportMode'; diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V6__Fix_DataModel_ID_Field.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V6__Fix_DataModel_ID_Field.sql new file mode 100644 index 000000000..f8b0f7f98 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V6__Fix_DataModel_ID_Field.sql @@ -0,0 +1,63 @@ +-- +-- Copyright (c) 2024 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - SQL Script +-- +-- + +create + or replace function pg_temp.migrate_distribution(distribution jsonb) returns jsonb as +$$ +declare + data_standard jsonb; + data_standard_path text[]; +begin + data_standard_path := '{https://w3id.org/mobilitydcat-ap/mobilityDataStandard}'; + data_standard := distribution #> data_standard_path; + + if jsonb_typeof(data_standard) = 'object' then + data_standard := pg_temp.migrate_mobility_data_standard(data_standard); + elsif jsonb_typeof(data_standard) = 'array' then + data_standard := (select jsonb_agg(pg_temp.migrate_mobility_data_standard(it)) + from jsonb_array_elements(data_standard) as it); + end if; + + return jsonb_set(distribution, data_standard_path, data_standard, true); +end; +$$ language plpgsql; + + +create + or replace function pg_temp.migrate_mobility_data_standard(data_standard jsonb) returns jsonb as +$$ +begin + return pg_temp.remove_if_blank(data_standard, '{@id}'); +end; +$$ language plpgsql; + + +create + or replace function pg_temp.remove_if_blank(obj jsonb, path text[]) returns jsonb as +$$ +declare + value text; +begin + value := obj #>> path; + if value is null or trim(value) = '' then + obj := obj #- path; + end if; + return obj; +end; +$$ language plpgsql; + + +update edc_asset_property +set property_value = pg_temp.migrate_distribution(property_value::jsonb)::text +where property_name = 'http://www.w3.org/ns/dcat#distribution'; diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V9__legacy_cleanup.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V9__legacy_cleanup.sql new file mode 100644 index 000000000..76f7e17f1 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V9__legacy_cleanup.sql @@ -0,0 +1,11 @@ +-- Clear the old migration tables if they exist once we reach this point. +-- These tables don't exist with this new line of updates +-- but may exist in older databases what started their lifetime with the older migration lines. +drop table if exists flyway_schema_history_asset; +drop table if exists flyway_schema_history_contractdefinition; +drop table if exists flyway_schema_history_contractnegotiation; +drop table if exists flyway_schema_history_dataplaneinstance; +drop table if exists flyway_schema_history_default; +drop table if exists flyway_schema_history_policy; +drop table if exists flyway_schema_history_transferprocess; + From f237870e94018bbe59bb5c1dd9012954458fb88d Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 19 Jun 2024 08:22:44 +0200 Subject: [PATCH 243/295] feat: Add utilities to access the database via jOOQ (#979) --- docs/api/sovity-edc-api-wrapper.yaml | 7 +- settings.gradle.kts | 1 + utils/jooq-database-access/README.md | 31 ++++ utils/jooq-database-access/build.gradle.kts | 170 ++++++++++++++++++++ 4 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 utils/jooq-database-access/README.md create mode 100644 utils/jooq-database-access/build.gradle.kts diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 03747c25a..64d290250 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -1589,14 +1589,13 @@ components: $ref: '#/components/schemas/ExpressionType' expressions: type: array - description: List of policy elements that are evaluated according the constraintType. + description: List of policy elements that are evaluated according the expressionType. items: $ref: '#/components/schemas/Expression' atomicConstraint: $ref: '#/components/schemas/AtomicConstraintDto' - description: Generic Policy Element. Represents a single atomic constraint or - a multiplicity constraint. The atomicConstraint will be evaluated if the constraintType - is ATOMIC. + description: Represents a single atomic constraint or a multiplicity constraint. + The atomicConstraint will be evaluated if the constraintType is ATOMIC_CONSTRAINT. ExpressionType: type: string description: | diff --git a/settings.gradle.kts b/settings.gradle.kts index b4c3bf3b3..176f5942d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,7 @@ include(":launchers:connectors:sovity-dev") include(":launchers:connectors:test-backend") include(":tests") include(":utils:catalog-parser") +include(":utils:jooq-database-access") include(":utils:json-and-jsonld-utils") include(":utils:test-connector-remote") include(":utils:test-utils") diff --git a/utils/jooq-database-access/README.md b/utils/jooq-database-access/README.md new file mode 100644 index 000000000..449d06242 --- /dev/null +++ b/utils/jooq-database-access/README.md @@ -0,0 +1,31 @@ + +
      +

      + + Logo + + +

      EDC-Connector Utilities:
      jOOQ direct database access utilities

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this module + +jOOQ Utilities + +## Why does this module exist? + +The jOOQ code and utilities from this repository are re-used in other sovity repositories. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/utils/jooq-database-access/build.gradle.kts b/utils/jooq-database-access/build.gradle.kts new file mode 100644 index 000000000..9ce518294 --- /dev/null +++ b/utils/jooq-database-access/build.gradle.kts @@ -0,0 +1,170 @@ +import org.flywaydb.gradle.task.FlywayCleanTask +import org.flywaydb.gradle.task.FlywayMigrateTask +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.containers.PostgreSQLContainer +import nu.studer.gradle.jooq.JooqGenerate +import org.jooq.meta.jaxb.ForcedType +import org.jooq.meta.jaxb.Nullability + +plugins { + `java-library` + `maven-publish` + alias(libs.plugins.flyway) + alias(libs.plugins.jooq) +} + +buildscript { + dependencies { + classpath(libs.testcontainers.postgresql) + } +} + +val flywayMigration = configurations.create("flywayMigration") + +dependencies { + jooqGenerator(libs.postgres) + flywayMigration(libs.postgres) + + annotationProcessor(libs.lombok) + + api(libs.jooq.jooq) + api(libs.t9tJooq.jooqPostgresqlJson) + + compileOnly(libs.lombok) +} + +var container: JdbcDatabaseContainer<*>? = null + +fun jdbcUrl(): String { + return container?.jdbcUrl ?: error("The test container didn't start!") +} + +fun jdbcUser(): String { + return container?.username ?: error("The test container didn't start!") +} + +fun jdbcPassword(): String { + return container?.password ?: error("The test container didn't start!") +} + +val startTestcontainer = tasks.register("startTestcontainer") { + doLast { + val postgresContainer = libs.versions.postgresDbImage.get() + container = PostgreSQLContainer(postgresContainer) + container!!.start() + gradle.buildFinished { + container?.stop() + } + } +} + + +val migrationsDir = "../../extensions/postgres-flyway/src/main/resources/db/migration" +val jooqTargetPackage = "de.sovity.edc.ext.db.jooq" + +val jooqTargetDir = "build/generated/jooq/" + jooqTargetPackage.replace(".", "/") + + +flyway { + driver = "org.postgresql.Driver" + schemas = arrayOf("public") + + cleanDisabled = false + cleanOnValidationError = true + baselineOnMigrate = true + locations = arrayOf("filesystem:${migrationsDir}") + configurations = arrayOf("flywayMigration") + + mixed = true +} + +tasks.withType { + doFirst { + require(this is FlywayCleanTask) + url = jdbcUrl() + user = jdbcUser() + password = jdbcPassword() + } +} + +tasks.withType { + dependsOn.add(startTestcontainer) + doFirst { + require(this is FlywayMigrateTask) + url = jdbcUrl() + user = jdbcUser() + password = jdbcPassword() + } +} + +jooq { + configurations { + create("main") { + jooqConfiguration.apply { + generator.apply { + database.apply { + name = "org.jooq.meta.postgres.PostgresDatabase" + excludes = "(.*)flyway_schema_history(.*)" + inputSchema = flyway.schemas[0] + + withForcedTypes( + // Force "List" over "String[]" for PostgreSQL "text[]" + ForcedType() + .withUserType("java.util.List") + .withIncludeTypes("_text|_varchar") + .withConverter("""org.jooq.Converter.ofNullable( + String[].class, + (Class>) (Class) java.util.List.class, + array -> array == null ? null : java.util.Arrays.asList(array), + list -> list == null ? null : list.toArray(new String[0]) + )""") + .withNullability(Nullability.ALL) + ) + } + generate.apply { + isRecords = true + isRelations = true + } + target.apply { + packageName = jooqTargetPackage + directory = jooqTargetDir + } + } + } + } + } +} + +tasks.withType { + dependsOn.add("flywayMigrate") + inputs.files(fileTree(migrationsDir)) + .withPropertyName("migrations") + .withPathSensitivity(PathSensitivity.RELATIVE) + allInputsDeclared.set(true) + outputs.cacheIf { true } + doFirst { + require(this is JooqGenerate) + + val jooqConfiguration = JooqGenerate::class.java.getDeclaredField("jooqConfiguration") + .also { it.isAccessible = true }.get(this) as org.jooq.meta.jaxb.Configuration + + jooqConfiguration.jdbc.apply { + url = jdbcUrl() + user = jdbcUser() + password = jdbcPassword() + } + } + doLast { + container?.stop() + } +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} From 268cd6f147fe557c8732892dbbf5cd2e12b3f99f Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:16:04 +0200 Subject: [PATCH 244/295] Update build.gradle.kts --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index aed53cee1..a46e46876 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -113,7 +113,7 @@ subprojects { repositories { maven { name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/sovity/edc-extensions") + url = uri("https://maven.pkg.github.com/sovity/edc-ce") credentials { username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") From f3ad2e0ca18dfc9837a99a01f1237ced9d15fb4b Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:39:18 +0200 Subject: [PATCH 245/295] Update markdown-link-checker-config.jq --- .github/markdown-link-checker-config.jq | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/markdown-link-checker-config.jq b/.github/markdown-link-checker-config.jq index 5e4e43556..7089c9efd 100755 --- a/.github/markdown-link-checker-config.jq +++ b/.github/markdown-link-checker-config.jq @@ -18,6 +18,14 @@ { "pattern": "^https://github.com/sovity/edc-extensions/tree/main/", "replacement": "https://github.com/sovity/edc-extensions/tree/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" + }, + { + "pattern": "^https://github.com/sovity/edc-ce/blob/main/", + "replacement": "https://github.com/sovity/edc-ce/blob/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" + }, + { + "pattern": "^https://github.com/sovity/edc-ce/tree/main/", + "replacement": "https://github.com/sovity/edc-ce/tree/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" } ] } From 6837b43306e0c6e58c22bfbab82f3f369d34294a Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:51:28 +0200 Subject: [PATCH 246/295] Update README.md --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2d2b8ca1d..7acfb1783 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@
      - + Logo @@ -21,9 +21,9 @@

      Extended EDC Connector by sovity.
      -Report Bug +Report Bug · -Request Feature +Request Feature

      @@ -53,7 +53,7 @@ for building dataspaces, exchanging data securely with ensured data sovereignty. enterprise-ready managed services like "Connector-as-a-Service", out-of-the-box fully configured DAPS and integrations to existing other dataspace technologies. -This repository contains our sovity Community Edition EDCs, containing pre-configured Open Source EDC Extensions. +This repository contains our sovity Community Edition EDCs, containing pre-configured open source EDC extensions. Check out our [Getting Started Section](#getting-started) on how to run a local sovity Community Edition EDC. @@ -63,16 +63,16 @@ Check out our [Getting Started Section](#getting-started) on how to run a local ## sovity Community Edition EDC -Our sovity Community Edition EDC takes available Open Source EDC Extensions and combines them with our own -open source EDC Extensions from this repository to build ready-to-use EDC Docker Images. +Our sovity Community Edition EDC takes available Open Source EDC extensions and combines them with our own +open source EDC extensions from this repository to build ready-to-use EDC Docker Images. See [here](launchers/README.md) for a list of our sovity Community Edition EDC Docker images.

      (back to top)

      -## sovity Community Edition EDC Extensions +## sovity Community Edition EDC extensions -Feel free to explore and use our [EDC Extensions](./extensions) with your EDC setup. +Feel free to explore and use our [EDC extensions](./extensions) with your EDC setup. We packaged critical extensions for compatibility with our EDC UI and general usability features into [sovity EDC Extensions Package](./extensions/sovity-edc-extensions-package). @@ -81,7 +81,7 @@ We packaged critical extensions for compatibility with our EDC UI and general us ## Compatibility -Our sovity Community Edition EDC and sovity Community Edition EDC Extensions are targeted to run with +Our sovity Community Edition EDC and it's extensions are targeted to run with our [sovity/edc-ui](https://github.com/sovity/edc-ui). Our sovity Community Edition EDC will use the current EDC Milestone with a certain delay @@ -111,7 +111,7 @@ appreciated**. If you have a suggestion that would improve this project, please fork the repo and create a pull request. You can also simply open -a [feature request](https://github.com/sovity/edc-extensions/issues/new?template=feature_request.md). Don't forget to +a [feature request](https://github.com/sovity/edc-ce/issues/new?template=feature_request.md). Don't forget to leave the project a ⭐, if you like the effort put into this version! Our contribution guideline can be found in [CONTRIBUTING.md](CONTRIBUTING.md). @@ -138,29 +138,29 @@ contact@sovity.de [contributors-shield]: -https://img.shields.io/github/contributors/sovity/edc-extensions.svg?style=for-the-badge +https://img.shields.io/github/contributors/sovity/edc-ce.svg?style=for-the-badge -[contributors-url]: https://github.com/sovity/edc-extensions/graphs/contributors +[contributors-url]: https://github.com/sovity/edc-ce/graphs/contributors [forks-shield]: -https://img.shields.io/github/forks/sovity/edc-extensions.svg?style=for-the-badge +https://img.shields.io/github/forks/sovity/edc-ce.svg?style=for-the-badge -[forks-url]: https://github.com/sovity/edc-extensions/network/members +[forks-url]: https://github.com/sovity/edc-ce/network/members [stars-shield]: -https://img.shields.io/github/stars/sovity/edc-extensions.svg?style=for-the-badge +https://img.shields.io/github/stars/sovity/edc-ce.svg?style=for-the-badge -[stars-url]: https://github.com/sovity/edc-extensions/stargazers +[stars-url]: https://github.com/sovity/edc-ce/stargazers [issues-shield]: -https://img.shields.io/github/issues/sovity/edc-extensions.svg?style=for-the-badge +https://img.shields.io/github/issues/sovity/edc-ce.svg?style=for-the-badge -[issues-url]: https://github.com/sovity/edc-extensions/issues +[issues-url]: https://github.com/sovity/edc-ce/issues [license-shield]: -https://img.shields.io/github/license/sovity/edc-extensions.svg?style=for-the-badge +https://img.shields.io/github/license/sovity/edc-ce.svg?style=for-the-badge -[license-url]: https://github.com/sovity/edc-extensions/blob/main/LICENSE +[license-url]: https://github.com/sovity/edc-ce/blob/main/LICENSE [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 From 82e17f504eaa6808efd7df1ec778b20df8d278fa Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:53:22 +0200 Subject: [PATCH 247/295] Update sovity-edc-api-wrapper.yaml --- docs/api/sovity-edc-api-wrapper.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 64d290250..5512293b2 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -6,22 +6,22 @@ info: \ etc. We bundled these APIs, so we can have an easier time generating our API\ \ Client Libraries." contact: - name: Sovity GmbH - url: https://github.com/sovity/edc-extensions/issues/new/choose + name: sovity GmbH + url: https://github.com/sovity/edc-ce/issues/new/choose email: contact@sovity.de license: name: Apache 2.0 - url: https://github.com/sovity/edc-extensions/blob/main/LICENSE + url: https://github.com/sovity/edc-ce/blob/main/LICENSE version: 0.0.0 externalDocs: - description: EDC API Wrapper Project in sovity/edc-extensions - url: https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper + description: EDC API Wrapper Project in sovity/edc-ce + url: https://github.com/sovity/edc-ce/tree/main/extensions/wrapper servers: - url: https://my-connector/api/management tags: - name: Enterprise Edition description: sovity Enterprise Edition EDC API Endpoints. Requires our sovity Enterprise - Edition EDC Extensions. + Edition EDC extensions. - name: UI description: EDC UI API Endpoints - name: Use Case From eec93ff94f5a3a2b3cd32ef1d675e62867f4b51b Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:55:00 +0200 Subject: [PATCH 248/295] Update postman_collection.json --- docs/api/postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 3da385756..7c9901f10 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -2,7 +2,7 @@ "info": { "_postman_id": "c01dc51f-eb36-43db-b8db-a33ff2f5baa5", "name": "sovity EDC Community Edition", - "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-extensions](https://github.com/sovity/edc-extensions)\n\nLicense: [https://github.com/sovity/edc-extensions/blob/main/LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE)", + "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "31514741" }, @@ -1916,7 +1916,7 @@ }, { "key": "PROVIDER_EDC_SOURCE_URL", - "value": "https://api.github.com/repos/sovity/edc-extensions/events", + "value": "https://api.github.com/repos/sovity/edc-ce/events", "type": "default" }, { From 489b5873814d81dce0932c45f28ff4ba55a27312 Mon Sep 17 00:00:00 2001 From: sovitybot <107936402+sovitybot@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:06:12 +0200 Subject: [PATCH 249/295] =?UTF-8?q?=F0=9F=94=84=20Templates:=20synced=20fi?= =?UTF-8?q?le(s)=20with=20sovity/PMO-Software=20(#981)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/process.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/add_issue_to_project.yml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/process.md b/.github/ISSUE_TEMPLATE/process.md index 4957c23cb..f49cf43b2 100644 --- a/.github/ISSUE_TEMPLATE/process.md +++ b/.github/ISSUE_TEMPLATE/process.md @@ -2,7 +2,7 @@ name: Refine Process Request about: Existing processes must be adapted or new ones created title: "" -labels: "task/documentation" +labels: ["task/refine-process","task/documentation"] assignees: "" --- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d324a40fb..63eeea83e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,6 +6,6 @@ _What issues does this PR close?_ - [ ] The PR title is short and expressive. - [ ] I have updated the CHANGELOG.md. See [changelog_update.md](https://github.com/sovity/authority-portal/tree/main/docs/dev/changelog_updates.md) for more information. - [ ] I have updated the Deployment Migration Notes Section in the CHANGELOG.md for any configuration / external API changes. -- [ ] I have updated the Community Edition [Postman-Collection](https://github.com/sovity/edc-extensions/blob/main/docs/api/postman_collection.json) if I changed existing APIs or added new APIs (e.g. for Management-API or API-Wrapper) +- [ ] I have updated the Community Edition [Postman-Collection](https://github.com/sovity/edc-ce/blob/main/docs/api/postman_collection.json) if I changed existing APIs or added new APIs (e.g. for Management-API or API-Wrapper) - [ ] I have performed a **self-review** ``` diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml index 76fd8814c..aa1b8c7bc 100644 --- a/.github/workflows/add_issue_to_project.yml +++ b/.github/workflows/add_issue_to_project.yml @@ -7,6 +7,7 @@ on: jobs: add_issue_to_project: + if: "!(startsWith(github.event.issue.title, '[Zammad Ticket') && github.event.issue.user.login == 'sovitybot')" name: add_issue_to_project runs-on: ubuntu-latest steps: From be21ec2e8cbc702b992f248670a4ca5cbb398439 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:25:43 +0200 Subject: [PATCH 250/295] chore(deps-dev): bump braces (#982) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../typescript-client/package-lock.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/extensions/wrapper/clients/typescript-client/package-lock.json b/extensions/wrapper/clients/typescript-client/package-lock.json index 28e667c34..666175b0e 100644 --- a/extensions/wrapper/clients/typescript-client/package-lock.json +++ b/extensions/wrapper/clients/typescript-client/package-lock.json @@ -1000,12 +1000,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1172,9 +1172,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2597,12 +2597,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "chalk": { @@ -2736,9 +2736,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" From 277ff92bb9c6b561fcc2b2e012b1340cc1960d24 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Tue, 25 Jun 2024 09:06:58 +0200 Subject: [PATCH 251/295] feat: custom C2C messages (#970) --- .editorconfig | 1 + CHANGELOG.md | 2 + docs/dev/checkstyle/checkstyle-config.xml | 2 +- extensions/sovity-messenger/README.md | 72 +++++++ extensions/sovity-messenger/build.gradle.kts | 71 +++++++ .../extension/messenger/SovityMessage.java | 38 ++++ .../extension/messenger/SovityMessenger.java | 117 ++++++++++++ .../messenger/SovityMessengerException.java | 32 ++++ .../messenger/SovityMessengerExtension.java | 111 +++++++++++ .../messenger/SovityMessengerRegistry.java | 102 ++++++++++ .../controller/SovityMessageController.java | 178 ++++++++++++++++++ .../edc/extension/messenger/impl/Handler.java | 19 ++ .../JsonObjectFromSovityMessageRequest.java | 44 +++++ .../JsonObjectFromSovityMessageResponse.java | 44 +++++ .../messenger/impl/MessageEmitter.java | 40 ++++ .../messenger/impl/MessageReceiver.java | 46 +++++ .../messenger/impl/ObjectMapperFactory.java | 27 +++ .../messenger/impl/SovityMessageRequest.java | 46 +++++ .../messenger/impl/SovityMessageResponse.java | 28 +++ .../messenger/impl/SovityMessengerStatus.java | 28 +++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../SovityMessengerExtensionE2eTest.java | 109 +++++++++++ .../messenger/controller/Answer.java | 33 ++++ .../messenger/controller/Payload.java | 33 ++++ .../SovityMessageControllerTest.java | 125 ++++++++++++ .../messenger/demo/SovityMessengerDemo.java | 73 +++++++ .../demo/SovityMessengerDemoTest.java | 133 +++++++++++++ .../messenger/demo/message/Addition.java | 40 ++++ .../messenger/demo/message/Answer.java | 34 ++++ .../messenger/demo/message/Failing.java | 32 ++++ .../demo/message/Multiplication.java | 38 ++++ .../messenger/demo/message/Signal.java | 23 +++ .../messenger/demo/message/Sqrt.java | 38 ++++ .../demo/message/UnregisteredMessage.java | 24 +++ .../edc/extension/messenger/dto/Addition.java | 37 ++++ .../edc/extension/messenger/dto/Answer.java | 33 ++++ .../messenger/dto/Multiplication.java | 35 ++++ .../messenger/dto/UnsupportedMessage.java | 23 +++ .../echo/SovityMessageRequestTest.java | 57 ++++++ .../messenger/impl/MessageEmitterTest.java | 57 ++++++ .../impl/SovityMessengerRegistryImplTest.java | 72 +++++++ .../messenger/impl/SovityMessengerTest.java | 62 ++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 2 + gradle/libs.versions.toml | 4 +- settings.gradle.kts | 1 + .../e2e/DataSourceParameterizationTest.java | 3 +- .../sovity/edc/utils/jsonld/vocab/Prop.java | 10 + .../config/ConnectorConfigFactory.java | 62 +++++- 48 files changed, 2233 insertions(+), 9 deletions(-) create mode 100644 extensions/sovity-messenger/README.md create mode 100644 extensions/sovity-messenger/build.gradle.kts create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java create mode 100644 extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java create mode 100644 extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java create mode 100644 extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension diff --git a/.editorconfig b/.editorconfig index 4476e7487..5d2208c46 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_size = 4 +ij_continuation_indent_size = 4 [*.ts] quote_type = single diff --git a/CHANGELOG.md b/CHANGELOG.md index 47082e289..24fd6e084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Add the SovityMessenger extension + #### Patch Changes - Unified database migration histories diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml index dc848298a..df0eaafa4 100644 --- a/docs/dev/checkstyle/checkstyle-config.xml +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -267,7 +267,7 @@ - +
      diff --git a/extensions/sovity-messenger/README.md b/extensions/sovity-messenger/README.md new file mode 100644 index 000000000..4d8a46a25 --- /dev/null +++ b/extensions/sovity-messenger/README.md @@ -0,0 +1,72 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Sovity Messenger

      + +

      + Report Bug + · + Request Feature +

      +
      + + +## About this Extension + +To provide a simpler way to exchange messages between EDCs while re-using the Dataspace's Connector-to-Connector authentication mechanisms, we created our own extension with a much simpler API surface omitting JSON-LD. + +## Why does this extension exist? + +Adding custom DSP messages to a vanilla EDC is verbose and requires the handling of JSON-LD and implementing your own Transformers. Since we do not care about JSON-LD we wanted a simpler API surface. + +## Architecture + +The sovity Messenger is implemented on top of the DSP messaging protocol and re-uses its exchange and authentication. + +It is abstracted from the internals of the DSP protocol such that changing the underlying implementation remains an option. + + +```mermaid +--- +title: Registering a handler +--- +sequenceDiagram + Caller ->> SovityMessengerRegistry: register(inputClass, intputType, handler) +``` + +```mermaid +--- +title: Sending a message +--- +sequenceDiagram + Caller ->>+SovityMessenger: send(resultClass, counterPartyAddress, payload) + SovityMessenger -->> RemoteMessageDispatcherRegistry: dispatch(genericMessage) + SovityMessenger -->> -Caller: Future + RemoteMessageDispatcherRegistry ->> +CustomMessageReceiverController: <> + CustomMessageReceiverController ->> CustomMessageReceiverController: processMessage(handler, payload) + CustomMessageReceiverController -->> -RemoteMessageDispatcherRegistry: <> + RemoteMessageDispatcherRegistry -->> SovityMessenger: <> + SovityMessenger ->> +Caller: Future + Caller ->> -Caller: future.get() +``` + +## Demo + +You can find a demo in the [demo](src/test/java/de/sovity/edc/extension/messenger/demo). + +The 2 key entry points are: + +- Register your message receiving by talking to the SovityMessageRegistry as demonstrated [here](src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java). +- Send messages by calling the SovityMessenger as shown [here](src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java). + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/sovity-messenger/build.gradle.kts b/extensions/sovity-messenger/build.gradle.kts new file mode 100644 index 000000000..815865804 --- /dev/null +++ b/extensions/sovity-messenger/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + + compileOnly(libs.lombok) + + implementation(project(":utils:json-and-jsonld-utils")) + + implementation(libs.edc.controlPlaneCore) + implementation(libs.edc.dspApiConfiguration) + implementation(libs.edc.dspHttpSpi) + implementation(libs.edc.httpSpi) + implementation(libs.edc.managementApiConfiguration) + implementation(libs.edc.transformCore) + + + testAnnotationProcessor(libs.lombok) + + testCompileOnly(libs.lombok) + + testImplementation(project(":utils:test-connector-remote")) + testImplementation(project(":utils:test-utils")) + + testImplementation(libs.edc.junit) + testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.edc.dspApiConfiguration) + testImplementation(libs.edc.dspHttpCore) + testImplementation(libs.edc.iamMock) + testImplementation(libs.edc.jsonLd) + + testImplementation(libs.edc.http) { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation(libs.bundles.jetty.cve2023) + + testImplementation(libs.assertj.core) + testImplementation(libs.junit.api) + testImplementation(libs.jsonAssert) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockserver.netty) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.testcontainers.junitJupiter) + testImplementation(libs.testcontainers.postgresql) + + testRuntimeOnly(libs.junit.engine) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java new file mode 100644 index 000000000..04d07f0a1 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * The interface to implement when sending a message via the {@link SovityMessenger}. + *
      + * The classes extending this interface must annotate the private fields to be sent with Jackson's + * {@link com.fasterxml.jackson.annotation.JsonProperty}. + * {@code public} fields are serialized automatically. + *
      + * It is recommended to have a no-args constructor. + *
      + * See this doc + * for more detailed info about Jackson's serialization. + */ +public interface SovityMessage { + /** + * To avoid conflicts, it is recommended to use Java package-like naming convention. + * + * @return A unique string across all possible messages to identify the type of message. + */ + @JsonIgnore + String getType(); +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java new file mode 100644 index 000000000..82dbfe54d --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.SovityMessengerStatus; +import de.sovity.edc.utils.JsonUtils; +import jakarta.json.Json; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.response.StatusResult; +import org.jetbrains.annotations.NotNull; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * The service to send {@link SovityMessage}s. + */ +@RequiredArgsConstructor +public class SovityMessenger { + + private final RemoteMessageDispatcherRegistry registry; + + private final ObjectMapper serializer; + + /** + * Sends a message to the counterparty address and returns a future result. + * + * @param resultType The result's class. + * @param counterPartyAddress The base DSP URL where to send the message. e.g. https://server:port/api/dsp + * @param payload The message to send. + * @param The outgoing message type. + * @param The incoming message type. + * @return A future result. + * @throws SovityMessengerException If a problem related to the message processing happened. + */ + public CompletableFuture> send( + Class resultType, String counterPartyAddress, T payload) { + try { + val message = buildMessage(counterPartyAddress, payload); + val future = registry.dispatch(SovityMessageRequest.class, message); + return future.thenApply(processResponse(resultType, payload)); + } catch (URISyntaxException | MalformedURLException | JsonProcessingException e) { + throw new EdcException("Failed to build a custom sovity message", e); + } + } + + static class Discarded implements SovityMessage { + @Override + public String getType() { + return "de.sovity.edc.extension.messenger.impl.SovityMessengerImpl.Discarded"; + } + } + + /** + * Fire-and-forget messaging where you don't care about the response. + */ + public void send(String counterPartyAddress, T payload) { + send(Discarded.class, counterPartyAddress, payload); + } + + @NotNull + private Function, StatusResult> processResponse( + Class resultType, T payload) { + return statusResult -> statusResult.map(content -> { + try { + val headerStr = content.header(); + val header = JsonUtils.parseJsonObj(headerStr); + if (header.getString("status").equals(SovityMessengerStatus.OK.getCode())) { + val resultBody = content.body(); + return serializer.readValue(resultBody, resultType); + } else if (header.getString("status").equals(SovityMessengerStatus.HANDLER_EXCEPTION.getCode())) { + throw new SovityMessengerException( + header.getString("message"), + header.getString(SovityMessengerStatus.HANDLER_EXCEPTION.getCode(), "No outgoing body."), + payload); + } else { + throw new SovityMessengerException(header.getString("message")); + } + } catch (JsonProcessingException e) { + throw new EdcException(e); + } + }); + } + + @NotNull + private SovityMessageRequest buildMessage(String counterPartyAddress, T payload) + throws MalformedURLException, URISyntaxException, JsonProcessingException { + val url = new URI(counterPartyAddress).toURL(); + val header1 = Json.createObjectBuilder() + .add("type", payload.getType()) + .build(); + val header = JsonUtils.toJson(header1); + val serialized = serializer.writeValueAsString(payload); + return new SovityMessageRequest(url, header, serialized); + } + +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java new file mode 100644 index 000000000..2de888e30 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger; + +import lombok.Getter; + +public class SovityMessengerException extends RuntimeException { + + @Getter + private String body; + private Object payload; + + public SovityMessengerException(String message) { + super(message); + } + + public SovityMessengerException(String message, String body, Object payload) { + super(message); + this.body = body; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java new file mode 100644 index 000000000..5a01dc7c0 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.controller.SovityMessageController; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageResponse; +import de.sovity.edc.extension.messenger.impl.MessageEmitter; +import de.sovity.edc.extension.messenger.impl.MessageReceiver; +import de.sovity.edc.extension.messenger.impl.ObjectMapperFactory; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import lombok.val; +import org.eclipse.edc.protocol.dsp.api.configuration.DspApiConfiguration; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpRemoteMessageDispatcher; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.web.spi.WebService; + +@Provides({SovityMessenger.class, SovityMessengerRegistry.class}) +public class SovityMessengerExtension implements ServiceExtension { + + public static final String NAME = "SovityMessenger"; + + @Inject + private DspApiConfiguration dspApiConfiguration; + + @Inject + private DspHttpRemoteMessageDispatcher dspHttpRemoteMessageDispatcher; + + @Inject + private IdentityService identityService; + + @Inject + private JsonLdRemoteMessageSerializer jsonLdRemoteMessageSerializer; + + @Inject + private Monitor monitor; + + @Inject + private RemoteMessageDispatcherRegistry registry; + + @Inject + private TypeManager typeManager; + + @Inject + private TypeTransformerRegistry typeTransformerRegistry; + + @Inject + private WebService webService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + val objectMapper = new ObjectMapperFactory().createObjectMapper(); + val handlers = new SovityMessengerRegistry(); + setupSovityMessengerEmitter(context, objectMapper); + setupSovityMessengerReceiver(context, objectMapper, handlers); + } + + private void setupSovityMessengerEmitter(ServiceExtensionContext context, ObjectMapper objectMapper) { + val factory = new MessageEmitter(jsonLdRemoteMessageSerializer); + val delegate = new MessageReceiver(objectMapper); + + dspHttpRemoteMessageDispatcher.registerMessage(SovityMessageRequest.class, factory, delegate); + + typeTransformerRegistry.register(new JsonObjectFromSovityMessageRequest()); + + val sovityMessenger = new SovityMessenger(registry, objectMapper); + context.registerService(SovityMessenger.class, sovityMessenger); + } + + private void setupSovityMessengerReceiver(ServiceExtensionContext context, ObjectMapper objectMapper, SovityMessengerRegistry handlers) { + val receiver = new SovityMessageController( + identityService, + dspApiConfiguration.getDspCallbackAddress(), + typeTransformerRegistry, + monitor, + objectMapper, + handlers); + + webService.registerResource(dspApiConfiguration.getContextAlias(), receiver); + + context.registerService(SovityMessengerRegistry.class, handlers); + + typeTransformerRegistry.register(new JsonObjectFromSovityMessageResponse()); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java new file mode 100644 index 000000000..933dc6a1f --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger; + +import de.sovity.edc.extension.messenger.impl.Handler; +import lombok.SneakyThrows; +import lombok.val; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * The component where to register message handlers when using the {@link SovityMessenger}. + */ +public class SovityMessengerRegistry { + + private final Map> handlers = new HashMap<>(); + + /** + * Register a handler to process a sovity message. + * + * @param incomingMessage The incoming message type to register (what was sent). Must have a no-arg constructor. + * @param handler The function to process this type of message. + * @param Incoming message type. + * @param Outgoing message type you send with {@link SovityMessenger#send(Class, String, SovityMessage)}. + */ + @SneakyThrows + public void register(Class incomingMessage, Function handler) { + val type = getTypeViaIntrospection(incomingMessage); + register(incomingMessage, type, handler); + } + + /** + * Registers a signal. This is a simplified version of a message where no answer is expected. + * + * @param incomingSignal The signal to send. + * @param handler Signal processing + */ + @SneakyThrows + public void registerSignal(Class incomingSignal, Consumer handler) { + val type = getTypeViaIntrospection(incomingSignal); + registerSignal(incomingSignal, type, handler); + } + + /** + * Use this constructor only if your message can't have a default constructor. Otherwise, prefer using + * {@link #register(Class, Function)} for type safety. + */ + public void register(Class clazz, String type, Function handler) { + if (handlers.containsKey(type)) { + throw new IllegalStateException("A handler is already registered for " + type); + } + + handlers.put(type, new Handler<>(clazz, handler)); + } + + /** + * Use this constructor only if your message can't have a default constructor. Otherwise, prefer using + * {@link #registerSignal(Class, Consumer)} for type safety. + */ + public void registerSignal(Class clazz, String type, Consumer handler) { + if (handlers.containsKey(type)) { + throw new IllegalStateException("A handler is already registered for " + type); + } + register(clazz, type, in -> { + handler.accept(in); + return null; + }); + } + + /** + * Retrieve a handler for the specified message type. + * + * @param type A unique ID to identify a message type. + * @return The function to process this message type. + */ + @SuppressWarnings("unchecked") + public Handler getHandler(String type) { + return (Handler) handlers.get(type); + } + + private static String getTypeViaIntrospection(Class incomingMessage) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + val defaultConstructor = incomingMessage.getConstructor(); + defaultConstructor.setAccessible(true); + val type = defaultConstructor.newInstance().getType(); + return type; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java new file mode 100644 index 000000000..5acc77c21 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.SovityMessage; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import de.sovity.edc.extension.messenger.impl.Handler; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.SovityMessageResponse; +import de.sovity.edc.extension.messenger.impl.SovityMessengerStatus; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.protocol.dsp.api.configuration.error.DspErrorResponse; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.UUID; + +import static de.sovity.edc.extension.messenger.controller.SovityMessageController.PATH; + +@RequiredArgsConstructor +@Path(PATH) +public class SovityMessageController { + + public static final String PATH = "/sovity/message/generic"; + + private final IdentityService identityService; + private final String callbackAddress; + private final TypeTransformerRegistry typeTransformerRegistry; + private final Monitor monitor; + private final ObjectMapper mapper; + + @Getter + private final SovityMessengerRegistry handlers; + + @POST + public Response post( + @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization, + SovityMessageRequest request) { + + val validation = validateToken(authorization); + if (validation.failed()) { + return Response.status( + Response.Status.UNAUTHORIZED.getStatusCode(), + String.join(", ", validation.getFailureMessages()) + ).build(); + } + + val handler = getHandler(request); + if (handler == null) { + val errorAnswer = buildErrorNoHandlerHeader(request); + return Response.ok() + .type(MediaType.APPLICATION_JSON) + .entity(errorAnswer).build(); + } + + try { + val response = processMessage(request, handler); + + return typeTransformerRegistry.transform(response, JsonObject.class) + .map(it -> Response.ok().type(MediaType.APPLICATION_JSON).entity(it).build()) + .orElse(failure -> { + var errorCode = UUID.randomUUID(); + monitor.warning(String.format("Error transforming " + response.getClass().getSimpleName() + ", error id %s: %s", errorCode, failure.getFailureDetail())); + return DspErrorResponse + .type(Prop.SovityMessageExt.REQUEST) + .internalServerError(); + }); + } catch (Exception e) { + monitor.warning("Failed to process message with type " + getMessageType(request), e); + val errorAnswer = buildErrorHandlerExceptionHeader(request); + return Response.ok() + .type(MediaType.APPLICATION_JSON) + .entity(errorAnswer) + .build(); + } + } + + private SovityMessageResponse processMessage(SovityMessageRequest compacted, Handler handler) throws JsonProcessingException { + val bodyStr = compacted.body(); + val parsed = mapper.readValue(bodyStr, handler.clazz()); + val result = handler.handler().apply(parsed); + val resultBody = mapper.writeValueAsString(result); + + val response = new SovityMessageResponse( + buildOkHeader(handler.clazz()), + resultBody); + + return response; + } + + private String buildOkHeader(Class clazz) { + try { + Constructor constructor = clazz.getConstructor(); + constructor.setAccessible(true); + String type = ((SovityMessage) constructor.newInstance()).getType(); + JsonObject header = Json.createObjectBuilder() + .add("status", SovityMessengerStatus.OK.getCode()) + .add("type", type) + .build(); + return JsonUtils.toJson(header); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new EdcException(e); + } + } + + private Result validateToken(String authorization) { + val token = TokenRepresentation.Builder.newInstance().token(authorization).build(); + return identityService.verifyJwtToken(token, callbackAddress); + } + + private SovityMessageResponse buildErrorNoHandlerHeader(SovityMessageRequest request) { + val messageType = getMessageType(request); + val json = Json.createObjectBuilder() + .add("status", SovityMessengerStatus.NO_HANDLER.getCode()) + .add("message", "No handler for message type " + messageType) + .build(); + val headerStr = JsonUtils.toJson(json); + + return new SovityMessageResponse(headerStr, ""); + } + + private SovityMessageResponse buildErrorHandlerExceptionHeader(SovityMessageRequest request) { + val messageType = getMessageType(request); + val body = request.body(); + val json = Json.createObjectBuilder() + .add("status", SovityMessengerStatus.HANDLER_EXCEPTION.getCode()) + .add("message", "Error when processing a message with type " + messageType) + .add(SovityMessengerStatus.HANDLER_EXCEPTION.getCode(), body) + .build(); + val headerStr = JsonUtils.toJson(json); + + return new SovityMessageResponse(headerStr, ""); + } + + private Handler getHandler(SovityMessageRequest request) { + final var messageType = getMessageType(request); + return handlers.getHandler(messageType); + } + + private static String getMessageType(SovityMessageRequest request) { + val headerStr = request.header(); + val header = Json.createReader(new StringReader(headerStr)).readObject(); + return header.getString("type"); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java new file mode 100644 index 000000000..4e7ef993b --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import java.util.function.Function; + +public record Handler(Class clazz, Function handler) { +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java new file mode 100644 index 000000000..baf15f102 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +public class JsonObjectFromSovityMessageRequest extends AbstractJsonLdTransformer { + + public JsonObjectFromSovityMessageRequest() { + super(SovityMessageRequest.class, JsonObject.class); + } + + @Override + public @Nullable JsonObject transform( + @NotNull SovityMessageRequest message, + @NotNull TransformerContext context) { + + var builder = Json.createObjectBuilder(); + builder.add(TYPE, Prop.SovityMessageExt.REQUEST) + .add(Prop.SovityMessageExt.HEADER, message.header()) + .add(Prop.SovityMessageExt.BODY, message.body()); + + return builder.build(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java new file mode 100644 index 000000000..9ae6a42e8 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +public class JsonObjectFromSovityMessageResponse extends AbstractJsonLdTransformer { + + public JsonObjectFromSovityMessageResponse() { + super(SovityMessageResponse.class, JsonObject.class); + } + + @Override + public @Nullable JsonObject transform( + @NotNull SovityMessageResponse message, + @NotNull TransformerContext context) { + + var builder = Json.createObjectBuilder(); + builder.add(TYPE, Prop.SovityMessageExt.RESPONSE) + .add(Prop.SovityMessageExt.HEADER, message.header()) + .add(Prop.SovityMessageExt.BODY, message.body()); + + return builder.build(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java new file mode 100644 index 000000000..1ba6bfb46 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import de.sovity.edc.extension.messenger.controller.SovityMessageController; +import lombok.RequiredArgsConstructor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpRequestFactory; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; + +@RequiredArgsConstructor +public class MessageEmitter implements DspHttpRequestFactory { + + private final JsonLdRemoteMessageSerializer serializer; + + @Override + public Request createRequest(SovityMessageRequest message) { + String serialized = serializer.serialize(message); + return new Request.Builder() + .url(message.counterPartyAddress() + SovityMessageController.PATH) + .post(RequestBody.create( + serialized, + MediaType.get(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + )) + .build(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java new file mode 100644 index 000000000..c997b9e4b --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.val; +import okhttp3.Response; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.spi.EdcException; + +import java.io.IOException; +import java.util.function.Function; + +@RequiredArgsConstructor +public class MessageReceiver extends DspHttpDispatcherDelegate { + + private final ObjectMapper mapper; + + @Override + protected Function parseResponse() { + return res -> { + try { + val body = res.body(); + if (body == null) { + return null; + } + String content = body.string(); + return mapper.readValue(content, SovityMessageRequest.class); + } catch (IOException e) { + throw new EdcException(e); + } + }; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java new file mode 100644 index 000000000..4c794e5ba --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +public class ObjectMapperFactory { + public ObjectMapper createObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + return objectMapper; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java new file mode 100644 index 000000000..4a057b76f --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.net.URL; + +public record SovityMessageRequest( + @JsonIgnore + URL counterPartyAddress, + + @JsonProperty(Prop.SovityMessageExt.HEADER) + String header, + + @JsonProperty(Prop.SovityMessageExt.BODY) + String body +) implements RemoteMessage { + + @JsonIgnore + @Override + public String getProtocol() { + return HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; + } + + @JsonIgnore + @Override + public String getCounterPartyAddress() { + return counterPartyAddress.toString(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java new file mode 100644 index 000000000..e6c79d291 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.utils.jsonld.vocab.Prop; + +public record SovityMessageResponse( + @JsonProperty(Prop.SovityMessageExt.HEADER) + String header, + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonProperty(Prop.SovityMessageExt.BODY) + String body +) { +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java new file mode 100644 index 000000000..516c16dce --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum SovityMessengerStatus { + + NO_HANDLER("no_handler"), + HANDLER_EXCEPTION("handler_exception"), + OK("ok"); + + private final String code; +} diff --git a/extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..7fde2514c --- /dev/null +++ b/extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.messenger.SovityMessengerExtension diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java new file mode 100644 index 000000000..dce553b83 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger; + +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.messenger.dto.Addition; +import de.sovity.edc.extension.messenger.dto.Answer; +import de.sovity.edc.extension.messenger.dto.Multiplication; +import de.sovity.edc.extension.messenger.dto.UnsupportedMessage; +import lombok.val; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.iam.TokenDecorator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +public class SovityMessengerExtensionE2eTest { + private static final String EMITTER_PARTICIPANT_ID = "emitter"; + private static final String RECEIVER_PARTICIPANT_ID = "receiver"; + + @RegisterExtension + static EdcExtension emitterEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension receiverEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase EMITTER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase RECEIVER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorConfig providerConfig; + private ConnectorConfig consumerConfig; + + private String counterPartyAddress; + + @BeforeEach + void setup() { + providerConfig = forTestDatabase(EMITTER_PARTICIPANT_ID, EMITTER_DATABASE); + emitterEdcContext.setConfiguration(providerConfig.getProperties()); + emitterEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + consumerConfig = forTestDatabase(RECEIVER_PARTICIPANT_ID, RECEIVER_DATABASE); + receiverEdcContext.setConfiguration(consumerConfig.getProperties()); + receiverEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + counterPartyAddress = consumerConfig.getProtocolEndpoint().getUri().toString(); + } + + @Test + void e2eTest() throws ExecutionException, InterruptedException, TimeoutException { + val sovityMessenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + val handlers = receiverEdcContext.getContext().getService(SovityMessengerRegistry.class); + handlers.register(Addition.class, in -> new Answer(in.getOp1() + in.getOp2())); + handlers.register(Multiplication.class, in -> new Answer(in.getOp1() * in.getOp2())); + + val added = sovityMessenger.send(Answer.class, counterPartyAddress, new Addition(20, 30)); + val multiplied = sovityMessenger.send(Answer.class, counterPartyAddress, new Multiplication(20, 30)); + + // assert + added.get(30, SECONDS) + .onFailure(it -> fail(it.getFailureDetail())) + .onSuccess(it -> { + assertThat(it).isInstanceOf(Answer.class); + assertThat(it.getAnswer()).isEqualTo(50); + }); + + multiplied.get(30, SECONDS) + .onFailure(it -> fail(it.getFailureDetail())) + .onSuccess(it -> { + assertThat(it).isInstanceOf(Answer.class); + assertThat(it.getAnswer()).isEqualTo(600); + }); + } + + @Test + void e2eNoHandlerTest() { + val sovityMessenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + + val added = sovityMessenger.send(Answer.class, counterPartyAddress, new UnsupportedMessage()); + + // assert + val exception = assertThrows(ExecutionException.class, () -> added.get(30, SECONDS)); + assertThat(exception.getCause()).isInstanceOf(SovityMessengerException.class); + assertThat(exception.getCause().getMessage()).isEqualTo("No handler for message type unsupported"); + } + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java new file mode 100644 index 000000000..e21839a13 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +class Answer implements SovityMessage { + @JsonProperty + private String string; + + @Override + public String getType() { + return "answer"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java new file mode 100644 index 000000000..6d791d5bf --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +class Payload implements SovityMessage { + @JsonProperty + private Integer integer; + + @Override + public String getType() { + return "payload"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java new file mode 100644 index 000000000..c297e442a --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageResponse; +import de.sovity.edc.extension.messenger.impl.ObjectMapperFactory; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import jakarta.ws.rs.core.Response; +import lombok.val; +import org.eclipse.edc.core.transform.TypeTransformerRegistryImpl; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.eclipse.edc.spi.result.Result; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +class SovityMessageControllerTest { + + private TypeTransformerRegistryImpl transformers = new TypeTransformerRegistryImpl(); + private ConsoleMonitor monitor = new ConsoleMonitor(); + private ObjectMapperFactory omf = new ObjectMapperFactory(); + private ObjectMapper objectMapper = omf.createObjectMapper(); + private IdentityService identityService = mock(IdentityService.class); + private SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + + @BeforeEach + public void beforeEach() { + transformers = new TypeTransformerRegistryImpl(); + transformers.register(new JsonObjectFromSovityMessageRequest()); + transformers.register(new JsonObjectFromSovityMessageResponse()); + + monitor = new ConsoleMonitor(); + + omf = new ObjectMapperFactory(); + objectMapper = omf.createObjectMapper(); + + handlers = new SovityMessengerRegistry(); + + reset(identityService); + when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.success(ClaimToken.Builder.newInstance().build())); + } + + @Test + void canAnswerRequest() throws JsonProcessingException, MalformedURLException { + // arrange + + val handlers = new SovityMessengerRegistry(); + + val controller = new SovityMessageController( + identityService, + "http://example.com/callback", + transformers, + monitor, + objectMapper, + handlers + ); + + Function handler = payload -> new Answer(String.valueOf(payload.getInteger())); + handlers.register(Payload.class, "foo", handler); + + val message = new SovityMessageRequest( + new URL("https://example.com/api"), + """ + { "type" : "foo" } + """, + objectMapper.writeValueAsString(new Payload(1))); + + // act + + try (val response = controller.post("", message)) { + // assert + assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + } + + } + + @Test + void post_whenNonAuthorized_shouldReturnHttp401() throws MalformedURLException, JsonProcessingException { + // arrange + val identityService = mock(IdentityService.class); + when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.failure("Invalid token")); + + val controller = new SovityMessageController(identityService, "http://example.com/callback", transformers, monitor, objectMapper, handlers); + + val message = new SovityMessageRequest( + new URL("https://example.com/api"), + """ + { "type" : "foo" } + """, + objectMapper.writeValueAsString(new Payload(1))); + + // act + try (val response = controller.post("", message)) { + // assert + assertThat(response.getStatus()).isEqualTo(Response.Status.UNAUTHORIZED.getStatusCode()); + } + } + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java new file mode 100644 index 000000000..1052df2b9 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo; + +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import de.sovity.edc.extension.messenger.demo.message.Addition; +import de.sovity.edc.extension.messenger.demo.message.Answer; +import de.sovity.edc.extension.messenger.demo.message.Failing; +import de.sovity.edc.extension.messenger.demo.message.Signal; +import de.sovity.edc.extension.messenger.demo.message.Sqrt; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static java.lang.Math.sqrt; + + +public class SovityMessengerDemo implements ServiceExtension { + + public static final String NAME = "sovityMessengerDemo"; + + @Override + public String name() { + return NAME; + } + + /* + * 3 parts are needed: + * - the messenger + * - the handler registry + * - your handlers + */ + + @Inject + private SovityMessenger sovityMessenger; + + @Inject + private SovityMessengerRegistry registry; + + @Override + public void initialize(ServiceExtensionContext context) { + // Register the various messages that you would like to process. + // By class, safer. + registry.register(Sqrt.class, single -> new Answer(sqrt(single.getValue()))); + // By String, could be unsafe during refactorings. + registry.register(Addition.class, Addition.TYPE, add -> new Answer(add.op1 + add.op2)); + + registry.registerSignal(Signal.class, signal -> System.out.println("Received signal.")); + registry.register(Failing.class, failing -> { + throw new RuntimeException("Failed!"); + }); + + /* + * In the counterpart connector, messages can be sent with the code below. + * Check out the de.sovity.edc.extension.sovitymessenger.demo.SovityMessengerDemoTest#demo() + * for a detailed usage. + */ + + // val answer = sovityMessenger.send(Answer.class, "http://localhost/api/dsp", new Sqrt(9.0)); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java new file mode 100644 index 000000000..afc357de9 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo; + +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.SovityMessengerException; +import de.sovity.edc.extension.messenger.demo.message.Addition; +import de.sovity.edc.extension.messenger.demo.message.Answer; +import de.sovity.edc.extension.messenger.demo.message.Failing; +import de.sovity.edc.extension.messenger.demo.message.Signal; +import de.sovity.edc.extension.messenger.demo.message.Sqrt; +import de.sovity.edc.extension.messenger.demo.message.UnregisteredMessage; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import lombok.val; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.iam.TokenDecorator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; + +class SovityMessengerDemoTest { + + // Setup, you may skip this part + + private static final String EMITTER_PARTICIPANT_ID = "emitter"; + private static final String RECEIVER_PARTICIPANT_ID = "receiver"; + + @RegisterExtension + static EdcExtension emitterEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension receiverEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase EMITTER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase RECEIVER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorConfig providerConfig; + private ConnectorConfig consumerConfig; + + private String receiverAddress; + + // still setup, skip + + @BeforeEach + void setup() { + providerConfig = forTestDatabase(EMITTER_PARTICIPANT_ID, EMITTER_DATABASE); + emitterEdcContext.setConfiguration(providerConfig.getProperties()); + emitterEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + consumerConfig = forTestDatabase(RECEIVER_PARTICIPANT_ID, RECEIVER_DATABASE); + receiverEdcContext.setConfiguration(consumerConfig.getProperties()); + receiverEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + receiverAddress = "http://localhost:" + consumerConfig.getProtocolEndpoint().port() + consumerConfig.getProtocolEndpoint().path(); + } + + /** + * Actual usage of the Sovity Messenger. + */ + @DisabledOnGithub + @Test + void demo() throws ExecutionException, InterruptedException, TimeoutException { + /* + * Get a reference to the SovityMessenger. This is equivalent to + * + * @Inject SovityMessenger messenger; + * + * in an extension. + * + * This messenger is already configured to accept messages in de.sovity.edc.extension.messenger.demo.SovityMessengerDemo#initialize + */ + val messenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + + System.out.println("START MARKER"); + + // Send messages + val added = messenger.send(Answer.class, receiverAddress, new Addition(20, 30)); + val rooted = messenger.send(Answer.class, receiverAddress, new Sqrt(9.0)); + val unregistered = messenger.send(Answer.class, receiverAddress, new UnregisteredMessage()); + messenger.send(receiverAddress, new Signal()); + + try { + // Wait for the answers + added.get(2, TimeUnit.SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); + rooted.get(2, TimeUnit.SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); + unregistered.get(2, TimeUnit.SECONDS); + } catch (ExecutionException e) { + /* + * When a problem happens, a SovityMessengerException is thrown and encapsulated in an ExecutionException. + */ + System.out.println(e.getCause().getMessage()); + } + + try { + val failing1 = messenger.send(Answer.class, receiverAddress, new Failing("Some content 1")); + val failing2 = messenger.send(Answer.class, receiverAddress, new Failing("Some content 2")); + failing1.get(2, TimeUnit.SECONDS); + failing2.get(2, TimeUnit.SECONDS); + } catch (ExecutionException e) { + val cause = e.getCause(); + if (cause instanceof SovityMessengerException messengerException) { + // Error when processing a message with type demo-failing + System.out.println(messengerException.getMessage()); + // {"message":"Some content 1/2"} + System.out.println(messengerException.getBody()); + } + } + + System.out.println("END MARKER"); + } + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java new file mode 100644 index 000000000..370a5c7bf --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Addition implements SovityMessage { + + public static final String TYPE = "demo-add"; + + @Override + public String getType() { + return TYPE; + } + + @JsonProperty + public int op1; + + @JsonProperty + public int op2; + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java new file mode 100644 index 000000000..8c79d1487 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Answer implements SovityMessage { + + @Override + public String getType() { + return "demo-answer"; + } + + @JsonProperty + private double answer; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java new file mode 100644 index 000000000..17dfa78be --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Failing implements SovityMessage { + + private String message; + + @Override + public String getType() { + return "demo-failing"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java new file mode 100644 index 000000000..f82c27dc3 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Multiplication implements SovityMessage { + + @Override + public String getType() { + return "demo-multiply"; + } + + @JsonProperty + public int op1; + + @JsonProperty + public int op2; + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java new file mode 100644 index 000000000..5cf54edff --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class Signal implements SovityMessage { + @Override + public String getType() { + return "demo-signal"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java new file mode 100644 index 000000000..b2011d507 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Sqrt implements SovityMessage { + + private static final String TYPE = "demo-sqrt"; + + @Override + public String getType() { + return TYPE; + } + + @JsonProperty + private double value; + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java new file mode 100644 index 000000000..5f1be9879 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class UnregisteredMessage implements SovityMessage { + + @Override + public String getType() { + return "demo-unregistered"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java new file mode 100644 index 000000000..a5f270e70 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Addition implements SovityMessage { + @Override + public String getType() { + return "add"; + } + + @JsonProperty + private int op1; + @JsonProperty + private int op2; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java new file mode 100644 index 000000000..29d3fa236 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Answer implements SovityMessage { + @Override + public String getType() { + return getClass().getCanonicalName(); + } + + @JsonProperty + private int answer; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java new file mode 100644 index 000000000..e25689bb6 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Multiplication implements SovityMessage { + @Override + public String getType() { + return "mul"; + } + + @JsonProperty + private int op1; + @JsonProperty + private int op2; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java new file mode 100644 index 000000000..9ccad0e52 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.dto; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class UnsupportedMessage implements SovityMessage { + @Override + public String getType() { + return "unsupported"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java new file mode 100644 index 000000000..c8df906ee --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.echo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.sovity.edc.extension.messenger.impl.ObjectMapperFactory; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import lombok.val; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.net.MalformedURLException; +import java.net.URL; + +class SovityMessageRequestTest { + + + @Test + void canSerialize() throws MalformedURLException, JsonProcessingException, JSONException { + // arrange + val message = new SovityMessageRequest( + new URL("https://example.com"), + "{\"type\":\"foo\"}", + "body content" + ); + + val mapper = new ObjectMapperFactory().createObjectMapper(); + + // act + val serialized = mapper.writeValueAsString(message); + + // assert + JSONAssert.assertEquals( + """ + { + "https://semantic.sovity.io/message/generic/header": "{\\"type\\":\\"foo\\"}", + "https://semantic.sovity.io/message/generic/body": "body content" + } + """, + serialized, + JSONCompareMode.STRICT + ); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java new file mode 100644 index 000000000..8369c7595 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import lombok.val; +import org.eclipse.edc.core.transform.TypeTransformerRegistryImpl; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.protocol.dsp.serialization.JsonLdRemoteMessageSerializerImpl; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class MessageEmitterTest { + + private final ObjectMapperFactory mapperFactory = new ObjectMapperFactory(); + + @Test + void emitValidMessage_whenEmpty_shouldSucceed() throws IOException { + // arrange + TypeTransformerRegistry registry = new TypeTransformerRegistryImpl(); + registry.register(new JsonObjectFromSovityMessageRequest()); + JsonLdRemoteMessageSerializer serializer = new JsonLdRemoteMessageSerializerImpl( + registry, + mapperFactory.createObjectMapper(), + new TitaniumJsonLd(mock(Monitor.class)) + ); + val emitter = new MessageEmitter(serializer); + + // act + val request = emitter.createRequest(new SovityMessageRequest( + new URL("https://example.com/api"), + "header", + "body" + )); + + // assert + assertThat(request.url().toString()).isEqualTo("https://example.com/api/sovity/message/generic"); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java new file mode 100644 index 000000000..b2c44cd72 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.val; +import org.junit.jupiter.api.Test; + +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SovityMessengerRegistryImplTest { + + @AllArgsConstructor + @NoArgsConstructor + @Getter + static class MyInt implements SovityMessage { + + @Override + public String getType() { + return "message"; + } + + @JsonProperty + private int value; + } + + @Test + void canRegisterAndRetrieveHandler() { + // arrange + SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + Function handler = myInt -> String.valueOf(myInt.getValue()); + + // act + handlers.register(MyInt.class, "itoa", handler); + val back = handlers.getHandler("itoa"); + + // assert + assertThat(back.handler().apply(new MyInt(1))).isEqualTo("1"); + } + + @Test + void register_whenRegisteringDuplicatedName_shouldThrowIllegalStateException() { + // arrange + SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + Function handler = myInt -> String.valueOf(myInt.getValue()); + + // act + handlers.register(MyInt.class, "foo", handler); + + // assert + assertThrows(IllegalStateException.class, () -> handlers.register(MyInt.class, "foo", handler)); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java new file mode 100644 index 000000000..6c2334d85 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.extension.messenger.impl; + +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.dto.Answer; +import de.sovity.edc.extension.messenger.dto.UnsupportedMessage; +import lombok.val; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.response.StatusResult; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SovityMessengerTest { + @Test + void send_whenNoHandler_shouldThrowSovityMessengerException() throws MalformedURLException { + // arrange + val registry = mock(RemoteMessageDispatcherRegistry.class); + CompletableFuture> future = CompletableFuture.completedFuture( + StatusResult.success( + new SovityMessageRequest( + new URL("https://example.com/api/dsp"), + """ + { + "status": "no_handler", + "message": "No handler for foo" + } + """, + null))); + + when(registry.dispatch(any(), any())).thenReturn(future); + val messenger = new SovityMessenger(registry, new ObjectMapperFactory().createObjectMapper()); + val answer = messenger.send(Answer.class, "https://example.com/api/dsp", new UnsupportedMessage()); + + // act + val exception = assertThrows(ExecutionException.class, answer::get); + + // assert + assertThat(exception.getCause().getMessage()).isEqualTo("No handler for foo"); + } +} diff --git a/extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..afe23dcb9 --- /dev/null +++ b/extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,2 @@ +de.sovity.edc.extension.messenger.SovityMessengerExtension +de.sovity.edc.extension.messenger.demo.SovityMessengerDemo diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 89770a97d..c124e45db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ postgres = "42.4.5" >>>>>>> refs/rewritten/main quarkus = "2.16.6.Final" quartz = "2.3.2" -restAssured = "4.5.0" +restAssured = "5.4.0" retry = "1.5.7" shadow = "7.1.2" swagger = "1.6.12" @@ -103,7 +103,9 @@ edc-dataPlaneUtil = { module = "org.eclipse.edc:data-plane-util", version.ref = edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-dspApiConfiguration = { module = "org.eclipse.edc:dsp-api-configuration", version.ref = "edc" } edc-dspHttpSpi = { module = "org.eclipse.edc:dsp-http-spi", version.ref = "edc" } +edc-dspHttpCore = { module = "org.eclipse.edc:dsp-http-core", version.ref = "edc" } edc-http = { module = "org.eclipse.edc:http", version.ref = "edc" } +edc-httpSpi = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } edc-iamMock = { module = "org.eclipse.edc:iam-mock", version.ref = "edc" } edc-jsonLd = { module = "org.eclipse.edc:json-ld", version.ref = "edc" } edc-jsonLdSpi = { module = "org.eclipse.edc:json-ld-spi", version.ref = "edc" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 176f5942d..a3ac33a58 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ include(":extensions:policy-always-true") include(":extensions:policy-referring-connector") include(":extensions:policy-time-interval") include(":extensions:postgres-flyway") +include(":extensions:sovity-messenger") include(":extensions:sovity-edc-extensions-package") include(":extensions:test-backend-controller") include(":extensions:transfer-process-status-checker") diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java index 1fbdce1c0..21d03fbeb 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -110,7 +110,6 @@ class DataSourceParameterizationTest { private final String destinationPath = "/destination/some/path/"; private final String sourceUrl = "http://localhost:" + port + sourcePath; private final String destinationUrl = "http://localhost:" + port + destinationPath; - // TODO: remove the test backend dependency? private ClientAndServer mockServer; private static final AtomicInteger DATA_OFFER_INDEX = new AtomicInteger(0); @@ -502,7 +501,7 @@ private String initiateTransferWithParameters( Map dataSinkProperties = new HashMap<>(); dataSinkProperties.put(EDC_NAMESPACE + "baseUrl", destinationUrl); dataSinkProperties.put(EDC_NAMESPACE + "method", HttpMethod.PUT); - dataSinkProperties.put(EDC_NAMESPACE + "type", "HttpData"); // TODO: http proxy + dataSinkProperties.put(EDC_NAMESPACE + "type", "HttpData"); transferProcessProperties.put(rootKey + METHOD, testCase.method); if (testCase.body != null) { diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index 3606ddde9..3d35377a0 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -136,6 +136,16 @@ public class HttpDatasourceHints { } } + @UtilityClass + public class SovityMessageExt { + public final String CTX = "https://semantic.sovity.io/message/generic/"; + public final String REQUEST = CTX + "request"; + public final String RESPONSE = CTX + "response"; + public final String ERROR_MESSAGE = CTX + "errorMessage"; + public final String HEADER = CTX + "header"; + public final String BODY = CTX + "body"; + } + /** * FOAF Vocabulary */ diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java index 5cc358f9a..6c7a046f8 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java @@ -16,22 +16,74 @@ import de.sovity.edc.extension.e2e.db.TestDatabase; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.val; +import java.io.IOException; +import java.net.ServerSocket; import java.util.HashMap; +import java.util.Random; import java.util.UUID; import static de.sovity.edc.extension.e2e.connector.config.DatasourceConfigUtils.configureDatasources; import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiConfigFactory.configureApi; +import static org.eclipse.edc.junit.testfixtures.TestUtils.MAX_TCP_PORT; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ConnectorConfigFactory { + private static final Random RANDOM = new Random(); + + /** + * Creates the default configuration to start an EDC with the given test database. + * + * @deprecated Use {@link ConnectorConfigFactory#forTestDatabase(String, TestDatabase)} + * with automatic ports allocation to prevent port allocation conflicts. + */ + @Deprecated public static ConnectorConfig forTestDatabase(String participantId, int firstPort, TestDatabase testDatabase) { var config = basicEdcConfig(participantId, firstPort); config.setProperties(configureDatasources(testDatabase.getJdbcCredentials())); return config; } + public static ConnectorConfig forTestDatabase(String participantId, TestDatabase testDatabase) { + val firstPort = getFreePortRange(5); + var config = basicEdcConfig(participantId, firstPort); + config.setProperties(configureDatasources(testDatabase.getJdbcCredentials())); + return config; + } + + private static synchronized int getFreePortRange(int size) { + // pick a random in a reasonable range + int firstPort = getFreePort(RANDOM.nextInt(10_000, 50_000)); + + int currentPort = firstPort; + do { + if (canUsePort(currentPort + 1)) { + currentPort++; + } else { + firstPort = getFreePort(currentPort++); + } + } while (currentPort < firstPort + size); + + return firstPort; + } + + private static boolean canUsePort(int port) { + + if (port <= 0 || port >= MAX_TCP_PORT) { + throw new IllegalArgumentException("Lower bound must be > 0 and < " + MAX_TCP_PORT + " and be < upperBound"); + } + + try (ServerSocket serverSocket = new ServerSocket(port)) { + serverSocket.setReuseAddress(true); + return true; + } catch (IOException e) { + return false; + } + } + public static ConnectorConfig basicEdcConfig(String participantId, int firstPort) { var apiKey = UUID.randomUUID().toString(); var apiConfig = configureApi(firstPort, apiKey); @@ -55,11 +107,11 @@ public static ConnectorConfig basicEdcConfig(String participantId, int firstPort properties.put("my.edc.maintainer.name", "Maintainer Name %s".formatted(participantId)); return new ConnectorConfig( - participantId, - apiConfig.getDefaultApiGroup(), - apiConfig.getManagementApiGroup(), - apiConfig.getProtocolApiGroup(), - properties + participantId, + apiConfig.getDefaultApiGroup(), + apiConfig.getManagementApiGroup(), + apiConfig.getProtocolApiGroup(), + properties ); } } From 5678a5449dc22c8b532b5fb10338b5a1316df64f Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 26 Jun 2024 16:41:47 +0200 Subject: [PATCH 252/295] feat: on request data offers, structured HttpData data sources via API (#983) --- .github/ISSUE_TEMPLATE/release.md | 24 +- .github/markdown-link-checker-config.jq | 8 +- CHANGELOG.md | 10 +- archived/broker/README.md | 16 +- build.gradle.kts | 5 + connector/README.md | 8 +- docs/api/sovity-edc-api-wrapper.yaml | 159 +++++- .../goals/broker-production/README.md | 6 +- .../goals/local-demo/4.2.0/README.md | 6 +- .../goals/production/4.2.0/README.md | 42 +- .../goals/production/README.md | 37 +- docs/dev/checkstyle/checkstyle-config.xml | 5 +- .../documentation/api_wrapper.md | 4 +- .../documentation/oauth-data-address.md | 12 +- .../ext/brokerserver/api/ApiInformation.java | 8 +- .../broker-server-api/client-ts/README.md | 2 +- .../broker-server-api/client/build.gradle.kts | 18 +- .../README.md | 6 +- extensions/broker-server/build.gradle.kts | 7 - .../BrokerServerExtensionContextBuilder.java | 71 ++- .../edc/ext/brokerserver/TestAsset.java | 60 +- .../refreshing/ConnectorUpdaterTest.java | 16 +- extensions/edc-ui-config/README.md | 6 +- extensions/last-commit-info/README.md | 10 +- extensions/policy-always-true/README.md | 6 +- .../policy/AlwaysTruePolicyExtensionTest.java | 11 +- .../policy-referring-connector/README.md | 6 +- .../AbstractReferringConnectorValidation.java | 3 +- ...tractReferringConnectorValidationTest.java | 11 +- extensions/policy-time-interval/README.md | 6 +- extensions/postgres-flyway/README.md | 6 +- .../sovity-edc-extensions-package/README.md | 6 +- extensions/sovity-messenger/README.md | 6 +- extensions/test-backend-controller/README.md | 6 +- .../transfer-process-status-checker/README.md | 6 +- extensions/wrapper/README.md | 6 +- .../clients/java-client-example/README.md | 6 +- .../wrapper/clients/java-client/README.md | 116 ++-- .../clients/java-client/build.gradle.kts | 21 +- .../typescript-client-example/README.md | 6 +- .../clients/typescript-client/README.md | 12 +- .../clients/typescript-client/package.json | 4 +- extensions/wrapper/wrapper-api/README.md | 6 +- .../edc/ext/wrapper/api/ApiInformation.java | 12 +- .../edc/ext/wrapper/api/ui/UiResource.java | 4 +- .../ext/wrapper/api/ui/model/AssetPage.java | 6 + .../api/ui/model/ContractAgreementCard.java | 12 +- .../api/ui/model/ContractAgreementPage.java | 7 + .../ContractAgreementTransferProcess.java | 12 +- .../api/ui/model/ContractDefinitionEntry.java | 8 + .../api/ui/model/ContractDefinitionPage.java | 7 +- .../ui/model/ContractDefinitionRequest.java | 12 +- .../ui/model/ContractNegotiationRequest.java | 12 +- .../ui/model/ContractNegotiationState.java | 12 +- .../api/ui/model/DashboardDapsConfig.java | 10 +- .../wrapper/api/ui/model/DashboardPage.java | 13 +- .../ui/model/DashboardTransferAmounts.java | 13 +- .../wrapper/api/ui/model/IdResponseDto.java | 14 +- .../model/InitiateCustomTransferRequest.java | 12 +- .../api/ui/model/InitiateTransferRequest.java | 12 +- .../api/ui/model/PolicyDefinitionPage.java | 6 + .../api/ui/model/TransferHistoryEntry.java | 8 + .../api/ui/model/TransferHistoryPage.java | 7 + .../api/ui/model/TransferProcessState.java | 12 +- .../api/ui/model/UiContractNegotiation.java | 12 +- .../wrapper/api/ui/model/UiContractOffer.java | 8 + .../ext/wrapper/api/ui/model/UiCriterion.java | 8 +- .../api/ui/model/UiCriterionLiteral.java | 14 +- .../api/ui/model/UiCriterionOperator.java | 8 +- .../ext/wrapper/api/ui/model/UiDataOffer.java | 8 + .../wrapper/api/usecase/UseCaseResource.java | 2 +- .../model/CatalogFilterExpression.java | 4 + .../model/CatalogFilterExpressionLiteral.java | 14 +- .../CatalogFilterExpressionOperator.java | 4 - .../api/usecase/model/CatalogQuery.java | 4 + .../wrapper/api/usecase/model/KpiResult.java | 12 +- .../usecase/model/PolicyCreateRequest.java | 12 +- .../model/TransferProcessStatesDto.java | 6 + .../wrapper/wrapper-common-api/README.md | 6 +- .../wrapper/api/common/model/AssetDto.java | 12 +- .../api/common/model/AtomicConstraintDto.java | 18 +- .../api/common/model/CriterionDto.java | 33 -- .../common/model/DataSourceAvailability.java | 27 + .../api/common/model/DataSourceType.java | 25 + .../wrapper/api/common/model/Expression.java | 12 +- .../api/common/model/PermissionDto.java | 18 +- .../model/PolicyDefinitionCreateRequest.java | 12 +- .../api/common/model/PolicyDefinitionDto.java | 12 +- .../ext/wrapper/api/common/model/UiAsset.java | 53 +- .../common/model/UiAssetCreateRequest.java | 37 +- ...taRequest.java => UiAssetEditRequest.java} | 19 +- .../api/common/model/UiDataSource.java | 57 ++ .../common/model/UiDataSourceHttpData.java | 89 +++ .../model/UiDataSourceHttpDataMethod.java | 28 + .../common/model/UiDataSourceOnRequest.java | 44 ++ .../wrapper/api/common/model/UiPolicy.java | 12 +- .../api/common/model/UiPolicyConstraint.java | 12 +- .../common/model/UiPolicyCreateRequest.java | 12 +- .../api/common/model/UiPolicyLiteral.java | 10 +- .../wrapper/wrapper-common-mappers/README.md | 6 +- .../api/common/mappers/AssetMapper.java | 99 +++- .../api/common/mappers/PolicyMapper.java | 24 +- .../mappers/asset/AssetEditRequestMapper.java | 78 +++ .../mappers/asset/AssetJsonLdBuilder.java | 291 ++++++++++ .../mappers/asset/AssetJsonLdParser.java | 216 +++++++ .../OwnConnectorEndpointService.java | 2 +- .../{ => asset}/utils/AssetJsonLdUtils.java | 2 +- .../{ => asset}/utils/EdcPropertyUtils.java | 6 +- .../utils/FailedMappingException.java | 2 +- .../mappers/asset/utils/JsonBuilderUtils.java | 76 +++ .../utils/ShortDescriptionBuilder.java} | 23 +- .../mappers/dataaddress/DataSourceMapper.java | 103 ++++ .../http/HttpDataSourceMapper.java | 133 +++++ .../dataaddress/http/HttpHeaderMapper.java | 60 ++ .../AtomicConstraintMapper.java | 3 +- .../ConstraintExtractor.java | 2 +- .../{utils => policy}/LiteralMapper.java | 3 +- .../{utils => policy}/MappingErrors.java | 2 +- .../mappers/{ => policy}/OperatorMapper.java | 2 +- .../{utils => policy}/PolicyValidator.java | 3 +- .../mappers/utils/JsonBuilderUtils.java | 79 --- .../api/common/mappers/utils/TextUtils.java | 23 - .../common/mappers/utils/UiAssetMapper.java | 374 ------------ .../api/common/mappers/AssetMapperTest.java | 263 --------- .../wrapper/api/common/mappers/Factory.java | 68 +++ .../api/common/mappers/JsonAssertsUtils.java | 111 ++++ .../api/common/mappers/PolicyMapperTest.java | 20 +- .../mappers/asset/AssetJsonLdBuilderTest.java | 538 ++++++++++++++++++ .../mappers/asset/AssetJsonLdParserTest.java | 462 +++++++++++++++ .../utils/EdcPropertyUtilsTest.java | 3 +- .../utils/ShortDescriptionBuilderTest.java | 104 ++++ .../AtomicConstraintMapperTest.java | 17 +- .../ConstraintExtractorTest.java | 20 +- .../{utils => policy}/LiteralMapperTest.java | 18 +- .../{utils => policy}/MappingErrorsTest.java | 19 +- .../{ => policy}/OperatorMapperTest.java | 17 +- .../PolicyValidatorTest.java | 34 +- .../common/mappers/utils/TextUtilsTest.java | 63 -- .../mappers/utils/UiAssetMapperTest.java | 306 ---------- ...sset.jsonld => example-asset-json-ld.json} | 16 +- .../src/test/resources/example-ui-asset.json | 53 ++ extensions/wrapper/wrapper-ee-api/README.md | 6 +- extensions/wrapper/wrapper/README.md | 6 +- extensions/wrapper/wrapper/build.gradle.kts | 4 - .../WrapperExtensionContextBuilder.java | 83 ++- .../edc/ext/wrapper/api/ApiInformation.java | 46 -- .../ext/wrapper/api/ui/UiResourceImpl.java | 6 +- .../api/ui/pages/asset/AssetApiService.java | 16 +- .../api/ui/pages/asset/AssetBuilder.java | 104 ---- .../ParameterizationCompatibilityUtils.java | 4 +- .../services/TransferRequestBuilder.java | 3 +- .../OwnConnectorEndpointServiceImpl.java | 2 +- .../ui/pages/asset/AssetApiServiceTest.java | 95 ++-- .../api/ui/pages/catalog/CatalogApiTest.java | 33 +- .../services/TransferRequestBuilderTest.java | 3 +- .../TransferProcessTestUtils.java | 3 +- .../api/usecase/UseCaseApiWrapperTest.java | 36 +- gradle/libs.versions.toml | 2 - launchers/README.md | 12 +- settings.gradle.kts | 2 +- tests/build.gradle.kts | 5 - .../de/sovity/edc/e2e/ApiWrapperDemoTest.java | 17 +- .../e2e/DataSourceParameterizationTest.java | 24 +- .../edc/e2e/DataSourceQueryParamsTest.java | 19 +- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 59 +- .../sovity/edc/e2e/UseCaseApiWrapperTest.java | 33 +- utils/catalog-parser/README.md | 6 +- utils/jooq-database-access/README.md | 6 +- utils/jooq-database-access/build.gradle.kts | 6 +- utils/json-and-jsonld-utils/README.md | 6 +- .../java/de/sovity/edc/utils/JsonUtils.java | 16 + .../sovity/edc/utils/jsonld/vocab/Prop.java | 15 + utils/test-connector-remote/README.md | 6 +- 173 files changed, 3984 insertions(+), 2115 deletions(-) delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceAvailability.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceType.java rename extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/{UiAssetEditMetadataRequest.java => UiAssetEditRequest.java} (93%) create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceOnRequest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => asset}/OwnConnectorEndpointService.java (88%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{ => asset}/utils/AssetJsonLdUtils.java (93%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{ => asset}/utils/EdcPropertyUtils.java (93%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{ => asset}/utils/FailedMappingException.java (91%) create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils/MarkdownToTextConverter.java => asset/utils/ShortDescriptionBuilder.java} (57%) create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/DataSourceMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/AtomicConstraintMapper.java (96%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/ConstraintExtractor.java (98%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/LiteralMapper.java (96%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/MappingErrors.java (95%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{ => policy}/OperatorMapper.java (93%) rename extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/PolicyValidator.java (96%) delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/JsonAssertsUtils.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParserTest.java rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{ => asset}/utils/EdcPropertyUtilsTest.java (91%) create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/AtomicConstraintMapperTest.java (95%) rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/ConstraintExtractorTest.java (82%) rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/LiteralMapperTest.java (92%) rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/MappingErrorsTest.java (53%) rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{ => policy}/OperatorMapperTest.java (74%) rename extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/{utils => policy}/PolicyValidatorTest.java (85%) delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java rename extensions/wrapper/wrapper-common-mappers/src/test/resources/{example-asset.jsonld => example-asset-json-ld.json} (93%) create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/resources/example-ui-asset.json delete mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java delete mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java rename extensions/wrapper/{wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils => wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services}/ParameterizationCompatibilityUtils.java (93%) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index d535389ec..1e37fe6e0 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -16,9 +16,9 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Decide a release version depending on major/minor/patch changes in the CHANGELOG.md. - [ ] Update this issue's title to the new version - [ ] `release-prep` PR: - - [ ] Write or review the current [Productive Deployment Guide](https://github.com/sovity/edc-extensions/blob/main/docs/deployment-guide/goals/production) - - [ ] Write or review the current [Development Deployment Guide](https://github.com/sovity/edc-extensions/blob/main/docs/deployment-guide/goals/development) - - [ ] Write or review the current [Local Demo Deployment Guide](https://github.com/sovity/edc-extensions/blob/main/docs/deployment-guide/goals/local-demo) + - [ ] Write or review the current [Productive Deployment Guide](https://github.com/sovity/edc-ce/blob/main/docs/deployment-guide/goals/production) + - [ ] Write or review the current [Development Deployment Guide](https://github.com/sovity/edc-ce/blob/main/docs/deployment-guide/goals/development) + - [ ] Write or review the current [Local Demo Deployment Guide](https://github.com/sovity/edc-ce/blob/main/docs/deployment-guide/goals/local-demo) - [ ] For Major version updates: If we want to continue supporting the old major version: - [ ] Keep the old Productive Development Guide in a separate location. - [ ] Add a note to the old version about its deprecation status. @@ -43,18 +43,18 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Remove empty sections from the patch notes. - [ ] Replace the existing `docker-compose.yaml` with `docker-compose-dev.yaml`. - [ ] Set the version for `EDC_IMAGE` of - the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] Set the version for `TEST_BACKEND_IMAGE` of - the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] Set the version for `BROKER_IMAGE` of - the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] Set the UI release version for `EDC_UI_IMAGE` of - the [docker-compose's .env file](https://github.com/sovity/edc-extensions/blob/main/.env). + the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] If the Eclipse EDC version changed, update - the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-extensions/blob/main/docs/api/eclipse-edc-management-api.yaml). - - [ ] Run all tests locally as long as the [GH flaky tests](https://github.com/sovity/edc-extensions/issues/870) are a problem. + the [eclipse-edc-management-api.yaml file](https://github.com/sovity/edc-ce/blob/main/docs/api/eclipse-edc-management-api.yaml). + - [ ] Run all tests locally as long as the [GH flaky tests](https://github.com/sovity/edc-ce/issues/870) are a problem. - [ ] Merge the `release-prep` PR. -- [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-extensions/actions). +- [ ] Wait for the main branch to be green. You can check the status in GH [actions](https://github.com/sovity/edc-ce/actions). - [ ] Validate the image - [ ] Pull the latest edc-dev image: `docker image pull ghcr.io/sovity/edc-dev:latest`. - [ ] Check that your image was built recently `docker image ls | grep ghcr.io/sovity/edc-dev`. @@ -66,12 +66,12 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Validate that the EDC is scanned. - [ ] Validate that the index is searchable. - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. -- [ ] [Create a release](https://github.com/sovity/edc-extensions/releases/new) +- [ ] [Create a release](https://github.com/sovity/edc-ce/releases/new) - [ ] In `Choose the tag`, type your new release version in the format `vx.y.z` (for instance `v1.2.3`) then click `+Create new tag vx.y.z on release`. - [ ] Re-use the changelog section as release description, and the version as title. - [ ] Check if the pipeline built the release versions in the Actions-Section (or you won't see it). - [ ] Revisit the changed list of tasks and compare it - with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-extensions/blob/main/.github/ISSUE_TEMPLATE/release.md). + with [.github/ISSUE_TEMPLATE/release.md](https://github.com/sovity/edc-ce/blob/main/.github/ISSUE_TEMPLATE/release.md). Propose changes where it makes sense. - [ ] Close this issue. - [ ] Inform the Product Manager of this new release diff --git a/.github/markdown-link-checker-config.jq b/.github/markdown-link-checker-config.jq index 7089c9efd..8aec895b1 100755 --- a/.github/markdown-link-checker-config.jq +++ b/.github/markdown-link-checker-config.jq @@ -12,12 +12,12 @@ ], "replacementPatterns": [ { - "pattern": "^https://github.com/sovity/edc-extensions/blob/main/", - "replacement": "https://github.com/sovity/edc-extensions/blob/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" + "pattern": "^https://github.com/sovity/edc-ce/blob/main/", + "replacement": "https://github.com/sovity/edc-ce/blob/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" }, { - "pattern": "^https://github.com/sovity/edc-extensions/tree/main/", - "replacement": "https://github.com/sovity/edc-extensions/tree/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" + "pattern": "^https://github.com/sovity/edc-ce/tree/main/", + "replacement": "https://github.com/sovity/edc-ce/tree/\(env | .CI_SHA // ("CI_SHA was null" | halt_error))/" }, { "pattern": "^https://github.com/sovity/edc-ce/blob/main/", diff --git a/CHANGELOG.md b/CHANGELOG.md index 24fd6e084..28c5ecee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ Support for Multiplicity Constraints in the API Wrapper. #### Minor Changes - API Wrapper - - Support for Multiplicity Constraints (https://github.com/sovity/edc-extensions/issues/968) + - Support for Multiplicity Constraints (https://github.com/sovity/edc-ce/issues/968) - Providing `Prop` class from `json-and-jsonld-utils` to the java-client to make relevant Constants available #### Patch Changes @@ -63,7 +63,7 @@ Support for Multiplicity Constraints in the API Wrapper. Starting from version `8`, the Broker has been merged with the Community edition. -[The former changelog](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for the Broker is still available but will not be updated anymore. +[The former changelog](https://github.com/sovity/edc-broker-server-extension/blob/v4.2.0/CHANGELOG.md) for the Broker is still available but will not be updated anymore. The Broker's version therefore jumps from version 4 to version 8. @@ -132,9 +132,9 @@ MDS Bugfix Release - Fixed naming of the `nutsLocations` field for MDS assets. - UI: Removed HTTP Verb "HEAD" as it was not supported by the backend - Docs: Updated image to explain data-transfer-methods -- Docs: Updated documentation for parameterization using [only the UI](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/parameterized_assets_via_ui.md) or the [Management-API](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/parameterized_assets.md) -- Docs: Updated [OAuth2 documentation](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/oauth-data-address.md) about necessary parameters that need to use the vault key instead of providing a secret directly -- Docs: Updated documentation for the [pull-data-transfer](https://github.com/sovity/edc-extensions/blob/main/docs/getting-started/documentation/pull-data-transfer.md) +- Docs: Updated documentation for parameterization using [only the UI](https://github.com/sovity/edc-ce/blob/main/docs/getting-started/documentation/parameterized_assets_via_ui.md) or the [Management-API](https://github.com/sovity/edc-ce/blob/main/docs/getting-started/documentation/parameterized_assets.md) +- Docs: Updated [OAuth2 documentation](https://github.com/sovity/edc-ce/blob/main/docs/getting-started/documentation/oauth-data-address.md) about necessary parameters that need to use the vault key instead of providing a secret directly +- Docs: Updated documentation for the [pull-data-transfer](https://github.com/sovity/edc-ce/blob/main/docs/getting-started/documentation/pull-data-transfer.md) - Dev Utils: Parallel test support for our Test Backend for some requests. ### Deployment Migration Notes diff --git a/archived/broker/README.md b/archived/broker/README.md index 4bf00c932..93e9ed983 100644 --- a/archived/broker/README.md +++ b/archived/broker/README.md @@ -116,7 +116,7 @@ Mid-development it might be un-pinned back to latest versions. ## Releasing -[Create a Release Issue](https://github.com/sovity/edc-extensions/issues/new?assignees=&labels=task%2Frelease%2Cscope%2Fmds&projects=&template=release.md&title=Release+x.x.x) and follow the instructions. +[Create a Release Issue](https://github.com/sovity/edc-ce/issues/new?assignees=&labels=task%2Frelease%2Cscope%2Fmds&projects=&template=release.md&title=Release+x.x.x) and follow the instructions.

      (back to top)

      @@ -124,12 +124,12 @@ Mid-development it might be un-pinned back to latest versions. ### Deployment Units -| Deployment Unit | Version / Details | -|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | -| Postgresql | 15 or compatible version | -| Broker Backend | broker-server-ce, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for compatible versions. | -| Broker UI | edc-ui, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/main/CHANGELOG.md) for compatible versions. | +| Deployment Unit | Version / Details | +|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | +| Postgresql | 15 or compatible version | +| Broker Backend | broker-server-ce, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/v4.2.0/CHANGELOG.md) for compatible versions. | +| Broker UI | edc-ui, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/v4.2.0/CHANGELOG.md) for compatible versions. | ### Configuration @@ -152,7 +152,7 @@ There is a [docker-compose.yaml](../../docker-compose.yaml) to try out the broke A productive configuration will require you to join a DAPS. For that you will need a SKI/AKI ClientID. Please refer -to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-extensions/tree/main/docs/getting-started#faq) +to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-ce/tree/main/docs/getting-started#faq) on how to generate one. The DAPS needs to contain the claim `referringConnector=broker` for the broker. diff --git a/build.gradle.kts b/build.gradle.kts index a46e46876..02b11e740 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -126,4 +126,9 @@ subprojects { withSourcesJar() withJavadocJar() } + + tasks.withType { + val fullOptions = options as StandardJavadocDocletOptions + fullOptions.addStringOption("Xdoclint:none", "-quiet") + } } diff --git a/connector/README.md b/connector/README.md index d7c86f88d..95b296ad4 100644 --- a/connector/README.md +++ b/connector/README.md @@ -18,10 +18,10 @@ The Broker Server is built in different variants: -| Docker Image | Type | Purpose | Features | -|------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| -| [broker-server-dev](https://github.com/sovity/edc-extensions/pkgs/container/broker-server-dev) | Development |
      • Local Deployment via our `docker-compose.yaml`
      • E2E Testing
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | -| [broker-server-ce](https://github.com/sovity/edc-extensions/pkgs/container/broker-server-ce) | Community Edition |
      • Productive Deployment
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | +| Docker Image | Type | Purpose | Features | +|----------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| [broker-server-dev](https://github.com/sovity/edc-ce/pkgs/container/broker-server-dev) | Development |
      • Local Deployment via our `docker-compose.yaml`
      • E2E Testing
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | +| [broker-server-ce](https://github.com/sovity/edc-ce/pkgs/container/broker-server-ce) | Community Edition |
      • Productive Deployment
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | ## Image Tags diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 5512293b2..fd30bbb14 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -2,9 +2,9 @@ openapi: 3.0.1 info: title: sovity EDC API Wrapper description: "sovity's EDC API Wrapper contains a selection of APIs for multiple\ - \ consumers, e.g. our EDC UI API, our generic Use Case API, our Commercial APIs,\ - \ etc. We bundled these APIs, so we can have an easier time generating our API\ - \ Client Libraries." + \ consumers, e.g. our EDC UI API, our generic Use Case API, our Commercial Edition\ + \ APIs, etc. We bundled these APIs, so we can have an easier time generating our\ + \ API Client Libraries." contact: name: sovity GmbH url: https://github.com/sovity/edc-ce/issues/new/choose @@ -21,7 +21,7 @@ servers: tags: - name: Enterprise Edition description: sovity Enterprise Edition EDC API Endpoints. Requires our sovity Enterprise - Edition EDC extensions. + Edition EDC Extensions. - name: UI description: EDC UI API Endpoints - name: Use Case @@ -208,7 +208,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UiAssetEditMetadataRequest' + $ref: '#/components/schemas/UiAssetEditRequest' responses: default: description: default response @@ -492,12 +492,22 @@ components: of 'null' or a negative value means that there are no limit. format: int32 description: Available and used resources of a connector. + DataSourceType: + type: string + description: Supported Data Source Types by UiDataSource + default: CUSTOM + enum: + - HTTP_DATA + - ON_REQUEST + - CUSTOM UiAssetCreateRequest: required: - - dataAddressProperties + - dataSource - id type: object properties: + dataSource: + $ref: '#/components/schemas/UiDataSource' id: type: string description: Asset Id @@ -591,12 +601,6 @@ components: type: string description: Temporal coverage end date (inclusive) format: date - dataAddressProperties: - type: object - additionalProperties: - type: string - description: Data Address - description: Data Address customJsonAsString: type: string description: Contains serialized custom properties in the JSON format. @@ -616,6 +620,95 @@ components: the private properties. The same limitations apply. description: Type-Safe OpenAPI generator friendly Asset Create DTO that supports an opinionated subset of the original EDC Asset Entity. + UiDataSource: + required: + - type + type: object + properties: + type: + $ref: '#/components/schemas/DataSourceType' + httpData: + $ref: '#/components/schemas/UiDataSourceHttpData' + onRequest: + $ref: '#/components/schemas/UiDataSourceOnRequest' + customProperties: + type: object + additionalProperties: + type: string + description: For all types. Custom Data Address Properties. + description: For all types. Custom Data Address Properties. + description: Data Offer Data Source Model. Supports certain Data Address types + but also leaves a backdoor for custom Data Address Properties. + UiDataSourceHttpData: + required: + - baseUrl + type: object + properties: + method: + $ref: '#/components/schemas/UiDataSourceHttpDataMethod' + baseUrl: + type: string + description: "HTTP Request URL. If parameterized, additional pathParams\ + \ will be joined onto existing one." + example: https://my-app.my-org.com/api/edc-data-offer/v1 + queryString: + type: string + description: HTTP Request Query Params / Query String. + example: search=example&limit=10 + headers: + type: object + additionalProperties: + type: string + description: HTTP Request Headers. HTTP Header Parameterization is not + available. + description: HTTP Request Headers. HTTP Header Parameterization is not available. + enableMethodParameterization: + type: boolean + description: "Enable Method Parameterization. This forces consumers to provide\ + \ a method, otherwise the transfer will fail." + default: false + enablePathParameterization: + type: boolean + description: Enable Path Parameterization. + default: false + enableQueryParameterization: + type: boolean + description: Enable Query Parameterization. Any additionally provided queryString + will be merged with the existing one. + default: false + enableBodyParameterization: + type: boolean + description: "Enable Body Parameterization. Forces the provider to provide\ + \ both a request body and a content type. Only Methods POST, PUT and PATCH\ + \ allow request bodies." + default: false + description: Only for type HTTP_DATA + UiDataSourceHttpDataMethod: + type: string + description: Supported HTTP Methods by UiDataSource + default: GET + enum: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + UiDataSourceOnRequest: + required: + - contactEmail + - contactPreferredEmailSubject + type: object + properties: + contactEmail: + type: string + description: Contact E-Mail address + example: contact@my-org.com + contactPreferredEmailSubject: + type: string + description: Contact Preferred E-Mail Subject + example: "Department XYZ Data Offer Request - My Product, My API" + description: ON_REQUEST type Data Source. IdResponseDto: required: - id @@ -774,9 +867,11 @@ components: - STRING - STRING_LIST - JSON - UiAssetEditMetadataRequest: + UiAssetEditRequest: type: object properties: + dataSourceOverrideOrNull: + $ref: '#/components/schemas/UiDataSource' title: type: string description: Asset Title @@ -896,16 +991,27 @@ components: items: $ref: '#/components/schemas/UiAsset' description: All data for the Asset Page + DataSourceAvailability: + type: string + description: Differentiate 'Live' Data Offers that have a real data source from + 'On Request' Data Offers that contain only a contact email address for requesting + an individual data offer. + enum: + - LIVE + - ON_REQUEST UiAsset: required: - assetId - connectorEndpoint - creatorOrganizationName + - dataSourceAvailability - isOwnConnector - participantId - title type: object properties: + dataSourceAvailability: + $ref: '#/components/schemas/DataSourceAvailability' assetId: type: string description: Asset Id @@ -921,6 +1027,15 @@ components: creatorOrganizationName: type: string description: Asset Organization Name + onRequestContactEmail: + type: string + description: Contact E-Mail address. Only for dataSourceAvailability ON_REQUEST. + example: contact@my-org.com + onRequestContactEmailSubject: + type: string + description: Contact Preferred E-Mail Subject. Only for dataSourceAvailability + ON_REQUEST. + example: "Department XYZ Data Offer Request - My Product, My API" language: type: string description: Asset Language @@ -957,16 +1072,20 @@ components: description: Homepage URL associated with the Asset httpDatasourceHintsProxyMethod: type: boolean - description: HTTP Datasource Hints Proxy Method + description: "HTTP Datasource Hint: Proxy Method. Only for dataSourceAvailability\ + \ LIVE with an underlying HTTP_DATA Data Source." httpDatasourceHintsProxyPath: type: boolean - description: HTTP Datasource Hints Proxy Path + description: "HTTP Datasource Hint: Proxy Path. Only for dataSourceAvailability\ + \ LIVE with an underlying HTTP_DATA Data Source." httpDatasourceHintsProxyQueryParams: type: boolean - description: HTTP Datasource Hints Proxy Query Params + description: "HTTP Datasource Hint: Proxy Query Params. Only for dataSourceAvailability\ + \ LIVE with an underlying HTTP_DATA Data Source." httpDatasourceHintsProxyBody: type: boolean - description: HTTP Datasource Hints Proxy Body + description: "HTTP Datasource Hint: Proxy Body. Only for dataSourceAvailability\ + \ LIVE with an underlying HTTP_DATA Data Source." dataCategory: type: string description: Data Category @@ -1169,6 +1288,7 @@ components: description: Contract Agreement Cards items: $ref: '#/components/schemas/ContractAgreementCard' + description: Data as required by the UI's Contract Agreement Page ContractAgreementTransferProcess: required: - lastUpdatedDate @@ -1247,7 +1367,6 @@ components: description: Contract Definition Entries items: $ref: '#/components/schemas/ContractDefinitionEntry' - description: All data for the Contract Definition Page ContractNegotiationSimplifiedState: type: string description: Simplified Contract Negotiation State to be used in UI @@ -1394,6 +1513,7 @@ components: $ref: '#/components/schemas/DashboardDapsConfig' connectorMiwConfig: $ref: '#/components/schemas/DashboardMiwConfig' + description: Data as required by the UI's Dashboard Page DashboardTransferAmounts: required: - numError @@ -1418,7 +1538,7 @@ components: type: integer description: Number of failed Transfer Processes format: int64 - description: Providing Transfer Process Amounts + description: Number of Transfer Processes for given direction. PolicyDefinitionDto: required: - policy @@ -1500,6 +1620,7 @@ components: description: Transfer History Page Entries items: $ref: '#/components/schemas/TransferHistoryEntry' + description: Data as required by the UI's Transfer History Page ContractNegotiationRequest: required: - assetId diff --git a/docs/deployment-guide/goals/broker-production/README.md b/docs/deployment-guide/goals/broker-production/README.md index 58d600ec7..dbc3da385 100644 --- a/docs/deployment-guide/goals/broker-production/README.md +++ b/docs/deployment-guide/goals/broker-production/README.md @@ -11,11 +11,11 @@ This is a productive deployment guide for self-hosting a functional sovity Broke | Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | | Postgresql | 15 or compatible version | | Broker Backend | broker-server-ce, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | -| Broker UI | edc-ui, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | +| Broker UI | edc-ui, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | ### Configuration -There is a [docker-compose.yaml](../../../../docker-compose.yaml) to try out the broker locally. +There is a [docker-compose.yaml](../../../../docker-compose.yaml) to try out the broker locally. However, a productive release will require a few more configuration options, so you should only use it to check if the released version is roughly working or if it's broken. @@ -36,7 +36,7 @@ so you should only use it to check if the released version is roughly working or A productive configuration will require you to join a DAPS. For that you will need a SKI/AKI ClientID. Please refer -to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-extensions/tree/main/docs/getting-started#faq) +to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-ce/tree/main/docs/getting-started#faq) on how to generate one. The DAPS needs to contain the claim `referringConnector=broker` for the broker. diff --git a/docs/deployment-guide/goals/local-demo/4.2.0/README.md b/docs/deployment-guide/goals/local-demo/4.2.0/README.md index 7120bb631..9880da6a9 100644 --- a/docs/deployment-guide/goals/local-demo/4.2.0/README.md +++ b/docs/deployment-guide/goals/local-demo/4.2.0/README.md @@ -8,7 +8,7 @@ Deployment Goal: Local Demo ## Quick Start To quickly start using our sovity EDC CE or MDS EDC CE, we offer a quick -start [docker-compose.yaml](https://github.com/sovity/edc-extensions/blob/v4.2.0/docker-compose.yaml) file. +start [docker-compose.yaml](https://github.com/sovity/edc-ce/blob/v4.2.0/docker-compose.yaml) file. @@ -23,7 +23,7 @@ start [docker-compose.yaml](https://github.com/sovity/edc-extensions/blob/v4.2.0 ```shell script # Run with Bash from the root directory of the project -# Use the release tag 4.2.0: https://github.com/sovity/edc-extensions/releases/tag/v4.2.0 +# Use the release tag 4.2.0: https://github.com/sovity/edc-ce/releases/tag/v4.2.0 # Log-In to the Github Container Registry docker login ghcr.io @@ -37,7 +37,7 @@ docker compose up ```shell script # Run with Bash from the root directory of the project -# Use the release tag 4.2.0: https://github.com/sovity/edc-extensions/releases/tag/v4.2.0 +# Use the release tag 4.2.0: https://github.com/sovity/edc-ce/releases/tag/v4.2.0 # Log-In to the Github Container Registry docker login ghcr.io diff --git a/docs/deployment-guide/goals/production/4.2.0/README.md b/docs/deployment-guide/goals/production/4.2.0/README.md index 92df18dc0..59adef4aa 100644 --- a/docs/deployment-guide/goals/production/4.2.0/README.md +++ b/docs/deployment-guide/goals/production/4.2.0/README.md @@ -39,30 +39,30 @@ The EDC Backend opens up multiple ports with different functionalities. They are proxy (at least the protocol endpoint needs to be). - The sovity EDC Connector is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `80` port. Henceforth, called the UI. - - The Backend's `11002` port. Henceforth, called the Management API. - - The Backend's `11003` port. Henceforth, called the Protocol API. + - The UI's `80` port. Henceforth, called the UI. + - The Backend's `11002` port. Henceforth, called the Management API. + - The Backend's `11003` port. Henceforth, called the Protocol API. - The mapping should look like this: - - `/api/v1/ids` -> `edc:11003/api/v1/ids` - - `/api/v1/management` -> `edc:11002/api/v1/management` - - All other requests should be mapped to `edc-ui:80` + - `/api/v1/ids` -> `edc:11003/api/v1/ids` + - `/api/v1/management` -> `edc:11002/api/v1/management` + - All other requests should be mapped to `edc-ui:80` - Regarding TLS/HTTPS: - - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. - - The UI and the Management API should have HTTP to HTTPS redirects. - - The Protocol API must allow HTTP traffic to pass through. This is due to some loopback requests - mistakenly using HTTP instead of HTTPS that would otherwise be blocked or have their credentials wiped. + - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. + - The UI and the Management API should have HTTP to HTTPS redirects. + - The Protocol API must allow HTTP traffic to pass through. This is due to some loopback requests + mistakenly using HTTP instead of HTTPS that would otherwise be blocked or have their credentials wiped. - Regarding Authentication: - - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full - control of the application. - - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space - Authority / DAPS and the configured certificates. + - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full + control of the application. + - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space + Authority / DAPS and the configured certificates. - Exposing to the internet: - - The Protocol API must be reachable via the internet. The required endpoints can be found in - this [public-endpoints.yaml](public-endpoints.yaml) - - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. + - The Protocol API must be reachable via the internet. The required endpoints can be found in + this [public-endpoints.yaml](public-endpoints.yaml) + - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. - Security: - - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). - - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. + - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). + - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. ## EDC UI Configuration @@ -93,8 +93,8 @@ A sovity EDC CE or MDS EDC CE Backend deployment requires: - The following configuration properties > [!WARNING] -> Please be careful with overriding any of the ENV Vars set in our [launchers/.env.connector](../../../../../launchers/.env.connector). -> Our defaults will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as +> Please be careful with overriding any of the ENV Vars set in our [launchers/.env.connector](../../../../../launchers/.env.connector). +> Our defaults will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as > trailing slashes. ```yaml diff --git a/docs/deployment-guide/goals/production/README.md b/docs/deployment-guide/goals/production/README.md index 3750324c0..be879d009 100644 --- a/docs/deployment-guide/goals/production/README.md +++ b/docs/deployment-guide/goals/production/README.md @@ -47,28 +47,28 @@ The EDC Backend opens up multiple ports with different functionalities. They are proxy (at least the protocol endpoint needs to be). - The sovity EDC Connector is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `8080` port. Henceforth, called the UI. - - The Backend's `11002` port. Henceforth, called the Management API. - - The Backend's `11003` port. Henceforth, called the Protocol API. + - The UI's `8080` port. Henceforth, called the UI. + - The Backend's `11002` port. Henceforth, called the Management API. + - The Backend's `11003` port. Henceforth, called the Protocol API. - The mapping should look like this: - - `https://[MY_EDC_FQDN]/api/dsp` -> `edc:11003/api/dsp` - - `https://[MY_EDC_FQDN]/api/management` -> **Auth Proxy** -> `edc:11002/api/management` - - All other requests -> **Auth Proxy** -> `edc-ui:80` + - `https://[MY_EDC_FQDN]/api/dsp` -> `edc:11003/api/dsp` + - `https://[MY_EDC_FQDN]/api/management` -> **Auth Proxy** -> `edc:11002/api/management` + - All other requests -> **Auth Proxy** -> `edc-ui:80` - Regarding TLS/HTTPS: - - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. - - All endpoint should have HTTP to HTTPS redirects. + - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. + - All endpoint should have HTTP to HTTPS redirects. - Regarding Authentication: - - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full - control of the application. - - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space - Authority / DAPS and the configured certificates. + - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full + control of the application. + - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space + Authority / DAPS and the configured certificates. - Exposing to the internet: - - The Protocol API must be reachable via the internet. The required endpoints can be found in - this [public-endpoints.yaml](public-endpoints.yaml) - - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. + - The Protocol API must be reachable via the internet. The required endpoints can be found in + this [public-endpoints.yaml](public-endpoints.yaml) + - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. - Security: - - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). - - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. + - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). + - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. ## EDC UI Configuration @@ -89,6 +89,7 @@ EDC_UI_CONFIG_URL: "edc-ui-config" ``` You can also optionally set the following config properties: + ```yaml # Override the management API URL shown to the user in the UI EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD: https://[EDC_URL]/api/control/management @@ -150,6 +151,7 @@ EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 ``` A LoggingHouse extension is included in the MDS variant, which means that additional properties must be set for it: + ```yaml # LoggingHouse Extension EDC_LOGGINGHOUSE_EXTENSION_ENABLED: "true" @@ -157,6 +159,7 @@ EDC_LOGGINGHOUSE_EXTENSION_URL: https://clearing.test.mobility-dataspace.eu ``` You can also optionally set the following config properties: + ```yaml # Enables DEBUG-Level logging DEBUG_LOGGING: true diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml index df0eaafa4..334a1d067 100644 --- a/docs/dev/checkstyle/checkstyle-config.xml +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -345,7 +345,10 @@ - + + + + diff --git a/docs/getting-started/documentation/api_wrapper.md b/docs/getting-started/documentation/api_wrapper.md index 5a79fd9d7..900fa4299 100644 --- a/docs/getting-started/documentation/api_wrapper.md +++ b/docs/getting-started/documentation/api_wrapper.md @@ -28,13 +28,13 @@ Maven: https://docs.github.com/en/packages/working-with-a-github-packages-regist - Gradle: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages - This might require a Github Personal Access Token (PAT) - Add the Java Client Library to your Maven/Gradle project: https://github.com/sovity/edc-extensions/packages/1825774 + Add the Java Client Library to your Maven/Gradle project: https://github.com/sovity/edc-ce/packages/1825774 Configuring The Client ======== - Configure the Client with either an API Key or OAuth2 Client - Credentials: https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper/clients/java-client#usage + Credentials: https://github.com/sovity/edc-ce/tree/main/extensions/wrapper/clients/java-client#usage - Your management API URL should look like https://your-connector-name.prod-sovity.azure.sovity.io/control/data Using The Client diff --git a/docs/getting-started/documentation/oauth-data-address.md b/docs/getting-started/documentation/oauth-data-address.md index e4d30f281..e178de93b 100644 --- a/docs/getting-started/documentation/oauth-data-address.md +++ b/docs/getting-started/documentation/oauth-data-address.md @@ -9,11 +9,11 @@ Data Sources and Data Sinks protected by OAuth2 OAuth2 protected APIs can be used for both Http-Data-Sources and Http-Data-Sinks. For both the following properties can be used: -| Property | Description | -|------------------------|--------------------------------------------------------------| -| oauth2:tokenUrl | Token-Url where the Access-Token can be fetched from | -| oauth2:clientId | The client id | -| oauth2:clientSecretKey | The vault key holding the client secret | +| Property | Description | +|------------------------|------------------------------------------------------| +| oauth2:tokenUrl | Token-Url where the Access-Token can be fetched from | +| oauth2:clientId | The client id | +| oauth2:clientSecretKey | The vault key holding the client secret | > [!NOTE] > The only supported flow right now is the "Client Credentials" flow. @@ -43,7 +43,7 @@ following request: `POST` to `https://{{FQDN}}/api/management/v3/assets` > [!IMPORTANT] -> Be aware that while all other API examples work with API `v2` this example requires API `v3` +> Be aware that while all other API examples work with API `v2` this example requires API `v3` ```json { diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java index 6e5f43ae0..81f868094 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java +++ b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java @@ -22,21 +22,21 @@ @OpenAPIDefinition( info = @Info( - title = "Broker Server API", + title = "Broker Server API (Deprecated)", version = "0.0.0", description = "Broker Server API for the Broker Server built by sovity.", contact = @Contact( name = "sovity GmbH", email = "contact@sovity.de", - url = "https://github.com/sovity/edc-extensions/issues/new/choose" + url = "https://github.com/sovity/edc-ce/issues/new/choose" ), license = @License( name = "Apache 2.0", - url = "https://github.com/sovity/edc-extensions/blob/main/LICENSE" + url = "https://github.com/sovity/edc-ce/blob/main/LICENSE" ) ), externalDocs = @ExternalDocumentation( - description = "Broker Server API in sovity/edc-broker-server-extension", + description = "Broker Server API in sovity/ce", url = "https://github.com/sovity/edc-broker-server-extension/tree/main/extensions/broker-server-api" ) ) diff --git a/extensions/broker-server-api/client-ts/README.md b/extensions/broker-server-api/client-ts/README.md index d76011324..93c38d954 100644 --- a/extensions/broker-server-api/client-ts/README.md +++ b/extensions/broker-server-api/client-ts/README.md @@ -48,7 +48,7 @@ let catalog: CatalogPageResult = await edcClient.brokerServerApi.catalogPage(); ## License Apache License 2.0 - see -[LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE) +[LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE) ## Contact diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts index 9150baa0d..efabfe7b2 100644 --- a/extensions/broker-server-api/client/build.gradle.kts +++ b/extensions/broker-server-api/client/build.gradle.kts @@ -11,6 +11,7 @@ repositories { // By using a separate configuration we can skip having the Extension Jar in our runtime classpath val openapiYaml = configurations.create("openapiGenerator") +val buildDir = layout.buildDirectory.get().asFile dependencies { // We only need the openapi.yaml file from this dependency @@ -40,7 +41,7 @@ tasks.getByName("test") { // Extract the openapi file from the JAR val openapiFileName = "broker-server.yaml" -val targetLocation = project.buildDir.resolve("openapi") +val targetLocation = buildDir.resolve("openapi") val extractOpenapiYaml by tasks.registering(Copy::class) { dependsOn(openapiYaml) into(targetLocation) @@ -58,7 +59,11 @@ val openApiGenerate = tasks.getByName("compileJava") { dependsOn(postprocessGeneratedClient) + options.compilerArgs = listOf("-Xlint:none") } val sourcesJar = tasks.getByName("sourcesJar") { diff --git a/extensions/broker-server-postgres-flyway-jooq/README.md b/extensions/broker-server-postgres-flyway-jooq/README.md index 3c25ebe04..ac9be06e0 100644 --- a/extensions/broker-server-postgres-flyway-jooq/README.md +++ b/extensions/broker-server-postgres-flyway-jooq/README.md @@ -1,16 +1,16 @@
      - + Logo

      Broker Server:
      PostgreSQL + Flyway + JooQ

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/broker-server/build.gradle.kts b/extensions/broker-server/build.gradle.kts index a304e1274..c53445e15 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/broker-server/build.gradle.kts @@ -1,6 +1,5 @@ plugins { `java-library` - alias(libs.plugins.retry) } configurations.all { @@ -56,12 +55,6 @@ dependencies { tasks.getByName("test") { useJUnitPlatform() - maxParallelForks = 1 - retry { - maxRetries.set(2) - maxFailures.set(4) - failOnPassedAfterRetry.set(false) - } } tasks.register("prepareKotlinBuildScriptModel") {} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java index 44e7290d2..8f78ffa67 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java @@ -80,17 +80,21 @@ import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.LiteralMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import de.sovity.edc.utils.catalog.DspCatalogService; import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; import lombok.NoArgsConstructor; @@ -178,22 +182,7 @@ public static BrokerServerExtensionContext buildContext( dataOfferWriter, dataOfferLimitsEnforcer ); - var edcPropertyUtils = new EdcPropertyUtils(); - var assetJsonLdUtils = new AssetJsonLdUtils(); - var markdownToTextConverter = new MarkdownToTextConverter(); - var textUtils = new TextUtils(); - var uiAssetMapper = new UiAssetMapper( - edcPropertyUtils, - assetJsonLdUtils, - markdownToTextConverter, - textUtils, - endpoint -> false - ); - var assetMapper = new AssetMapper( - typeTransformerRegistry, - uiAssetMapper, - jsonLd - ); + var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd); var fetchedDataOfferBuilder = new FetchedCatalogBuilder(assetMapper); var dspDataOfferBuilder = new DspDataOfferBuilder(jsonLd); var dspCatalogService = new DspCatalogService( @@ -372,4 +361,34 @@ private static ObjectMapper getJsonLdObjectMapper(TypeManager typeManager) { return objectMapper; } + + @NotNull + private static AssetMapper newAssetMapper(TypeTransformerRegistry typeTransformerRegistry, JsonLd jsonLd) { + var edcPropertyUtils = new EdcPropertyUtils(); + var assetJsonLdUtils = new AssetJsonLdUtils(); + var assetEditRequestMapper = new AssetEditRequestMapper(); + var shortDescriptionBuilder = new ShortDescriptionBuilder(); + var assetJsonLdParser = new AssetJsonLdParser( + assetJsonLdUtils, + shortDescriptionBuilder, + endpoint -> false + ); + var httpHeaderMapper = new HttpHeaderMapper(); + var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); + var dataSourceMapper = new DataSourceMapper( + edcPropertyUtils, + httpDataSourceMapper + ); + var assetJsonLdBuilder = new AssetJsonLdBuilder( + dataSourceMapper, + assetJsonLdParser, + assetEditRequestMapper + ); + return new AssetMapper( + typeTransformerRegistry, + assetJsonLdBuilder, + assetJsonLdParser, + jsonLd + ); + } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java index 9dba67370..5680a20c1 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java @@ -16,18 +16,23 @@ import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.utils.jsonld.vocab.Prop; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; import jakarta.json.JsonObject; import lombok.AccessLevel; import lombok.NoArgsConstructor; - -import java.util.Map; +import org.jetbrains.annotations.NotNull; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TestAsset { @@ -41,15 +46,14 @@ public static JsonObject getAssetJsonLd(String assetId) { } public static JsonObject getAssetJsonLd(UiAssetCreateRequest request) { - return getUiAssetMapper().buildAssetJsonLd( - request.toBuilder() - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.BASE_URL, "https://example.com" - )) - .build(), - "orgName" - ); + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .build()) + .build(); + var withDataSource = request.toBuilder().dataSource(dataSource).build(); + return buildAssetJsonLdBuilder().createAssetJsonLd(withDataSource, "orgName"); } /** @@ -73,13 +77,23 @@ public static void setDataOfferAssetMetadata(DataOfferRecord dataOfferRecord, Js dataOfferRecordUpdater.updateDataOffer(dataOfferRecord, fetchedDataOffer, false); } - public static UiAssetMapper getUiAssetMapper() { - return new UiAssetMapper( + public static AssetJsonLdBuilder buildAssetJsonLdBuilder() { + return new AssetJsonLdBuilder( + new DataSourceMapper( new EdcPropertyUtils(), - new AssetJsonLdUtils(), - new MarkdownToTextConverter(), - new TextUtils(), - "http://own-connector-endpoint"::equals + new HttpDataSourceMapper(new HttpHeaderMapper()) + ), + buildAssetJsonLdParser(), + new AssetEditRequestMapper() + ); + } + + @NotNull + private static AssetJsonLdParser buildAssetJsonLdParser() { + return new AssetJsonLdParser( + new AssetJsonLdUtils(), + new ShortDescriptionBuilder(), + "https://my-connector"::equals ); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java index 797db9607..3e8b82369 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java @@ -16,12 +16,15 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.ext.brokerserver.AssertionUtils; import de.sovity.edc.ext.brokerserver.BrokerServerExtensionContext; import de.sovity.edc.ext.brokerserver.TestUtils; @@ -171,6 +174,13 @@ public void createContractDefinition(String policyId, String assetId) { } private String createAsset() { + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://some.url") + .build()) + .build(); + return providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() .id("asset-1") .title("AssetName") @@ -186,11 +196,7 @@ private String createAsset() { .transportMode("transportMode") .keywords(List.of("keyword1", "keyword2")) .publisherHomepage("publisherHomepage") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, "http://some.url" - )) + .dataSource(dataSource) .customJsonAsString("{\"a\":\"x\"}") .customJsonLdAsString("{\"http://unknown/b\":{\"http://unknown/c\":\"y\"}}") .privateCustomJsonAsString("{\"a-private\":\"x-private\"}") diff --git a/extensions/edc-ui-config/README.md b/extensions/edc-ui-config/README.md index 6acd43b70..a94f98334 100644 --- a/extensions/edc-ui-config/README.md +++ b/extensions/edc-ui-config/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      EDC UI Extension Config

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/last-commit-info/README.md b/extensions/last-commit-info/README.md index ea959cd9f..4ee2777c5 100644 --- a/extensions/last-commit-info/README.md +++ b/extensions/last-commit-info/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Last Commit Info

      - Report Bug + Report Bug · - Request Feature + Request Feature

      @@ -28,7 +28,7 @@ Date: Mon Mar 13 08:09:15 2023 +0100 chore: update to milestone-8 Jar Last Commit Info: -commit 2fe06beaf6027fb4cc06db2adb7d5b4c8ae61b05 (HEAD -> 2023-03-16-edc-extensions-cleanup, origin/2023-03-16-edc-extensions-cleanup) +commit 2fe06beaf6027fb4cc06db2adb7d5b4c8ae61b05 (HEAD -> 2023-03-16-edc-ce-cleanup, origin/2023-03-16-edc-ce-cleanup) Author: First Last Date: Thu Mar 9 14:50:20 2023 +0100 @@ -45,7 +45,7 @@ We found that finding the last commit of the EDC Connector Image was the most ac running EDC Connector instance. Since our EDC Images use our EDC Extensions from this repository we also embed a second "jar last commit info" -during build time of the edc-extensions, which represent all other EDC Extensions of this repository, since +during build time of the edc-ce, which represent all other EDC Extensions of this repository, since they will always be used with the same version. ## Configuration diff --git a/extensions/policy-always-true/README.md b/extensions/policy-always-true/README.md index fe61a0f87..7f5ed233c 100644 --- a/extensions/policy-always-true/README.md +++ b/extensions/policy-always-true/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Always True Policy

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java b/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java index 6824ef12c..0919d269e 100644 --- a/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java +++ b/extensions/policy-always-true/src/test/java/de/sovity/edc/extension/policy/AlwaysTruePolicyExtensionTest.java @@ -20,16 +20,14 @@ import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.policy.engine.spi.PolicyContext; import org.eclipse.edc.policy.engine.spi.PolicyEngine; -import org.eclipse.edc.spi.agent.ParticipantAgent; import org.eclipse.edc.spi.protocol.ProtocolWebhook; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import java.util.Map; - import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -57,7 +55,7 @@ void alwaysTruePolicyDef(PolicyEngine policyEngine, var result = policyEngine.evaluate( ContractDefinitionResolver.CATALOGING_SCOPE, alwaysTrue.getPolicy(), - participantAgent() + mock(PolicyContext.class) ); // assert @@ -70,9 +68,4 @@ private PolicyDefinition alwaysTruePolicy(PolicyDefinitionService policyDefiniti requireNonNull(alwaysTrue, "Policy Definition does not exist: " + POLICY_DEFINITION_ID); return alwaysTrue; } - - @NotNull - private ParticipantAgent participantAgent() { - return new ParticipantAgent(Map.of(), Map.of()); - } } diff --git a/extensions/policy-referring-connector/README.md b/extensions/policy-referring-connector/README.md index b6c97689a..8ac089c52 100644 --- a/extensions/policy-referring-connector/README.md +++ b/extensions/policy-referring-connector/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Referring Connector Restricted Policy

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java index 8fb09c350..130005b88 100644 --- a/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java +++ b/extensions/policy-referring-connector/src/main/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidation.java @@ -26,6 +26,7 @@ import org.eclipse.edc.policy.engine.spi.PolicyContext; import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.agent.ParticipantAgent; import org.eclipse.edc.spi.monitor.Monitor; import java.util.Arrays; @@ -69,7 +70,7 @@ protected boolean evaluate(final Operator operator, final Object rightValue, fin return false; } - final var claims = policyContext.getParticipantAgent().getClaims(); + final var claims = policyContext.getContextData(ParticipantAgent.class).getClaims(); if (!claims.containsKey(REFERRING_CONNECTOR_CLAIM)) { return false; diff --git a/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java b/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java index 31d072abc..9f929b92d 100644 --- a/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java +++ b/extensions/policy-referring-connector/src/test/java/de/sovity/edc/extension/policy/functions/AbstractReferringConnectorValidationTest.java @@ -39,6 +39,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; class AbstractReferringConnectorValidationTest { @@ -55,7 +56,7 @@ void beforeEach() { this.policyContext = Mockito.mock(PolicyContext.class); this.participantAgent = Mockito.mock(ParticipantAgent.class); - Mockito.when(policyContext.getParticipantAgent()).thenReturn(participantAgent); + when(policyContext.getContextData(ParticipantAgent.class)).thenReturn(participantAgent); validation = new AbstractReferringConnectorValidation(monitor) {}; } @@ -179,17 +180,17 @@ void testValidationForMultipleParticipants() { } private void prepareContextProblems(List problems) { - Mockito.when(policyContext.getProblems()).thenReturn(problems); + when(policyContext.getProblems()).thenReturn(problems); if (problems == null || problems.isEmpty()) { - Mockito.when(policyContext.hasProblems()).thenReturn(false); + when(policyContext.hasProblems()).thenReturn(false); } else { - Mockito.when(policyContext.hasProblems()).thenReturn(true); + when(policyContext.hasProblems()).thenReturn(true); } } private void prepareReferringConnectorClaim(String referringConnector) { - Mockito.when(participantAgent.getClaims()) + when(participantAgent.getClaims()) .thenReturn(Collections.singletonMap("referringConnector", referringConnector)); } } diff --git a/extensions/policy-time-interval/README.md b/extensions/policy-time-interval/README.md index e6c543c77..599693cc9 100644 --- a/extensions/policy-time-interval/README.md +++ b/extensions/policy-time-interval/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Time Interval Restricted Policy

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/postgres-flyway/README.md b/extensions/postgres-flyway/README.md index 8ee2998ba..c5bbe489f 100644 --- a/extensions/postgres-flyway/README.md +++ b/extensions/postgres-flyway/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      PostgreSQL + Flyway

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/sovity-edc-extensions-package/README.md b/extensions/sovity-edc-extensions-package/README.md index ed0986757..e412aa056 100644 --- a/extensions/sovity-edc-extensions-package/README.md +++ b/extensions/sovity-edc-extensions-package/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension Package:
      sovity's Core EDC Extensions

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/sovity-messenger/README.md b/extensions/sovity-messenger/README.md index 4d8a46a25..a145f7e4b 100644 --- a/extensions/sovity-messenger/README.md +++ b/extensions/sovity-messenger/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Sovity Messenger

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/test-backend-controller/README.md b/extensions/test-backend-controller/README.md index 12d5e1e5a..9e59b11b1 100644 --- a/extensions/test-backend-controller/README.md +++ b/extensions/test-backend-controller/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Test Backend

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/transfer-process-status-checker/README.md b/extensions/transfer-process-status-checker/README.md index ab773f763..359aea3a8 100644 --- a/extensions/transfer-process-status-checker/README.md +++ b/extensions/transfer-process-status-checker/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      Transfer Process Status Checker

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/README.md b/extensions/wrapper/README.md index 1fabd0c4b..0d463cde7 100644 --- a/extensions/wrapper/README.md +++ b/extensions/wrapper/README.md @@ -1,16 +1,16 @@
      - + Logo

      sovity EDC API Wrapper

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/clients/java-client-example/README.md b/extensions/wrapper/clients/java-client-example/README.md index 57cdfd6fe..9216ab8f6 100644 --- a/extensions/wrapper/clients/java-client-example/README.md +++ b/extensions/wrapper/clients/java-client-example/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Quarkus Example Project

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/clients/java-client/README.md b/extensions/wrapper/clients/java-client/README.md index ffbe6c0a6..9eb0cd231 100644 --- a/extensions/wrapper/clients/java-client/README.md +++ b/extensions/wrapper/clients/java-client/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Java API Client

      - Report Bug + Report Bug · - Request Feature + Request Feature

      @@ -27,7 +27,7 @@ An example project using this client can be found [here](../java-client-example) de.sovity.edc client - ${sovity-edc-extensions.version} + ${sovity-edc-ce.version} ``` @@ -49,20 +49,20 @@ import de.sovity.edc.client.gen.model.KpiResult; */ public class WrapperClientExample { - public static final String CONNECTOR_ENDPOINT = "http://localhost:11002/api/management/v2"; - public static final String CONNECTOR_API_KEY = "..."; + public static final String CONNECTOR_ENDPOINT = "http://localhost:11002/api/management/v2"; + public static final String CONNECTOR_API_KEY = "..."; - public static void main(String[] args) { - // Configure Client - EdcClient client = EdcClient.builder() - .managementApiUrl(CONNECTOR_ENDPOINT) - .managementApiKey(CONNECTOR_API_KEY) - .build(); + public static void main(String[] args) { + // Configure Client + EdcClient client = EdcClient.builder() + .managementApiUrl(CONNECTOR_ENDPOINT) + .managementApiKey(CONNECTOR_API_KEY) + .build(); - // EDC API Wrapper APIs are now available for use - KpiResult kpiResult = client.useCaseApi().getKpis(); - System.out.println(kpiResult); - } + // EDC API Wrapper APIs are now available for use + KpiResult kpiResult = client.useCaseApi().getKpis(); + System.out.println(kpiResult); + } } ``` @@ -80,26 +80,26 @@ import de.sovity.edc.client.oauth2.SovityKeycloakUrl; */ public class WrapperClientExample { - public static final String CONNECTOR_ENDPOINT = - "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/data"; - public static final String CLIENT_ID = "{{your-connector}}-app"; - public static final String CLIENT_SECRET = "..."; - - public static void main(String[] args) { - // Configure Client - EdcClient client = EdcClient.builder() - .managementApiUrl(CONNECTOR_ENDPOINT) - .oauth2ClientCredentials(OAuth2ClientCredentials.builder() - .tokenUrl(SovityKeycloakUrl.PRODUCTION) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .build()) - .build(); - - // EDC API Wrapper APIs are now available for use - KpiResult kpiResult = client.useCaseApi().getKpis(); - System.out.println(kpiResult); - } + public static final String CONNECTOR_ENDPOINT = + "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/data"; + public static final String CLIENT_ID = "{{your-connector}}-app"; + public static final String CLIENT_SECRET = "..."; + + public static void main(String[] args) { + // Configure Client + EdcClient client = EdcClient.builder() + .managementApiUrl(CONNECTOR_ENDPOINT) + .oauth2ClientCredentials(OAuth2ClientCredentials.builder() + .tokenUrl(SovityKeycloakUrl.PRODUCTION) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .build()) + .build(); + + // EDC API Wrapper APIs are now available for use + KpiResult kpiResult = client.useCaseApi().getKpis(); + System.out.println(kpiResult); + } } ``` @@ -107,15 +107,15 @@ public class WrapperClientExample { Below are the examples of various tasks and the corresponding methods to be used from the Java-client. -| Task | Java-Client method | -|------------------------------------------------------|-------------------------------------------------------------------------| -| Create Policy - uiAPI | `EdcClient.uiApi().createPolicyDefinition(policyDefinition)` | -| Create Policy - useCaseApi (allows AND/OR/XOR operators) | `EdcClient.useCaseApi().createPolicyDefinitionUseCase(createRequest)` | -| Create asset (Asset Creation after activate) | `EdcClient.uiApi().createAsset(uiAssetRequest)` | -| Create contract definition | `EdcClient.uiApi().createContractDefinition(contractDefinition)` | -| Create Offer on consumer dashboard (Catalog Browser) | `EdcClient.uiApi().getCatalogPageDataOffers(PROTOCOL_ENDPOINT)` | -| Accept contract (Contract Negotiation) | `EdcClient.uiApi().initiateContractNegotiation(negotiationRequest)` | -| Transfer Data (Initiate Transfer) | `EdcClient.uiApi().initiateTransfer(negotiation)` | +| Task | Java-Client method | +|----------------------------------------------------------|-----------------------------------------------------------------------| +| Create Policy - uiAPI | `EdcClient.uiApi().createPolicyDefinition(policyDefinition)` | +| Create Policy - useCaseApi (allows AND/OR/XOR operators) | `EdcClient.useCaseApi().createPolicyDefinitionUseCase(createRequest)` | +| Create asset (Asset Creation after activate) | `EdcClient.uiApi().createAsset(uiAssetRequest)` | +| Create contract definition | `EdcClient.uiApi().createContractDefinition(contractDefinition)` | +| Create Offer on consumer dashboard (Catalog Browser) | `EdcClient.uiApi().getCatalogPageDataOffers(PROTOCOL_ENDPOINT)` | +| Accept contract (Contract Negotiation) | `EdcClient.uiApi().initiateContractNegotiation(negotiationRequest)` | +| Transfer Data (Initiate Transfer) | `EdcClient.uiApi().initiateTransfer(negotiation)` | These methods facilitate various operations such as creating policies, assets, contract definitions, browsing offers, accepting contracts, and initiating data transfers. @@ -128,28 +128,28 @@ var policyId = UUID.randomUUID().toString(); var membershipElement = buildAtomicElement("Membership", OperatorDto.EQ, "active"); var purposeElement = buildAtomicElement("PURPOSE", OperatorDto.EQ, "ID 3.1 Trace"); var andElement = new Expression() - .expressionType(ExpressionTypeDto.AND) - .expressions(List.of(membershipElement, purposeElement)); + .expressionType(ExpressionTypeDto.AND) + .expressions(List.of(membershipElement, purposeElement)); var permissionDto = new PermissionDto(andElement); var createRequest = new PolicyCreateRequest(policyId, permissionDto); var response = client.useCaseApi().createPolicyDefinitionUseCase(createRequest); private Expression buildAtomicElement( - String left, - OperatorDto operator, - String right) { - var atomicConstraint = new AtomicConstraintDto() - .leftExpression(left) - .operator(operator) - .rightExpression(right); - return new Expression() - .expressionType(ExpressionTypeDto.ATOMIC_CONSTRAINT) - .atomicConstraint(atomicConstraint); + String left, + OperatorDto operator, + String right) { + var atomicConstraint = new AtomicConstraintDto() + .leftExpression(left) + .operator(operator) + .rightExpression(right); + return new Expression() + .expressionType(ExpressionTypeDto.ATOMIC_CONSTRAINT) + .atomicConstraint(atomicConstraint); } ``` -The complete example can be seen in [this test](https://github.com/sovity/edc-extensions/blob/main/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java). +The complete example can be seen in [this test](https://github.com/sovity/edc-ce/blob/main/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java). ## License diff --git a/extensions/wrapper/clients/java-client/build.gradle.kts b/extensions/wrapper/clients/java-client/build.gradle.kts index 1f86c6774..33ecca1fe 100644 --- a/extensions/wrapper/clients/java-client/build.gradle.kts +++ b/extensions/wrapper/clients/java-client/build.gradle.kts @@ -11,6 +11,7 @@ repositories { // By using a separate configuration we can skip having the Extension Jar in our runtime classpath val openapiYaml = configurations.create("openapiGenerator") +val buildDir = layout.buildDirectory.get().asFile dependencies { // We only need the openapi.yaml file from this dependency @@ -43,7 +44,7 @@ tasks.getByName("test") { val openapiFile = "sovity-edc-api-wrapper.yaml" val extractOpenapiYaml by tasks.registering(Copy::class) { dependsOn(openapiYaml) - into("${project.buildDir}") + into("$buildDir") from(zipTree(openapiYaml.singleFile)) { include(openapiFile) } @@ -58,20 +59,24 @@ tasks.getByName("op "apiPackage" to "de.sovity.edc.client.gen.api", "modelPackage" to "de.sovity.edc.client.gen.model", "caseInsensitiveResponseHeaders" to "true", - "additionalModelTypeAnnotations" to "@lombok.AllArgsConstructor\n@lombok.Builder", + "additionalModelTypeAnnotations" to listOf( + "@lombok.AllArgsConstructor", + "@lombok.Builder", + "@SuppressWarnings(\"all\")" + ).joinToString("\n"), "annotationLibrary" to "swagger1", "hideGenerationTimestamp" to "true", "useRuntimeException" to "true", ) ) - inputSpec.set("${project.buildDir}/${openapiFile}") - outputDir.set("${project.buildDir}/generated/client-project") + inputSpec.set("${buildDir}/${openapiFile}") + outputDir.set("${buildDir}/generated/client-project") } val postprocessGeneratedClient by tasks.registering(Copy::class) { dependsOn("openApiGenerate") - from("${project.buildDir}/generated/client-project/src/main/java") + from("${buildDir}/generated/client-project/src/main/java") // @lombok.Builder clashes with the following generated model file. // It is the base class for OAS3 polymorphism via allOf/anyOf, which we won't use anyway. @@ -81,9 +86,9 @@ val postprocessGeneratedClient by tasks.registering(Copy::class) { // It was again only required for the polymorphism, which we won't use anyway. filter { if (it == "import javax.ws.rs.core.GenericType;") "" else it } - into("${project.buildDir}/generated/sources/openapi/java/main") + into("${buildDir}/generated/sources/openapi/java/main") } -sourceSets["main"].java.srcDir("${project.buildDir}/generated/sources/openapi/java/main") +sourceSets["main"].java.srcDir("${buildDir}/generated/sources/openapi/java/main") checkstyle { // Checkstyle loathes the generated files @@ -91,9 +96,9 @@ checkstyle { this.sourceSets = emptyList() } - tasks.getByName("compileJava") { dependsOn("postprocessGeneratedClient") + options.compilerArgs = listOf("-Xlint:none") } java { diff --git a/extensions/wrapper/clients/typescript-client-example/README.md b/extensions/wrapper/clients/typescript-client-example/README.md index d8fd5aff2..df3402089 100644 --- a/extensions/wrapper/clients/typescript-client-example/README.md +++ b/extensions/wrapper/clients/typescript-client-example/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      TypeScript API Client Example

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/clients/typescript-client/README.md b/extensions/wrapper/clients/typescript-client/README.md index 38897a1d0..c0238c73c 100644 --- a/extensions/wrapper/clients/typescript-client/README.md +++ b/extensions/wrapper/clients/typescript-client/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      TypeScript API Client

      - Report Bug + Report Bug · - Request Feature + Request Feature

      @@ -20,7 +20,7 @@ TypeScript Client Library to be imported and used in arbitrary applications like frontends or NodeJS projects. You can find our API Wrapper Project -[here](https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper). +[here](https://github.com/sovity/edc-ce/tree/main/extensions/wrapper). ## How to install @@ -46,7 +46,7 @@ let kpiData: KpiResult = await edcClient.useCaseApi.getKpis(); ``` A minimal example project using the typescript API client can be found -[here](https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper/clients/typescript-client-example). +[here](https://github.com/sovity/edc-ce/tree/main/extensions/wrapper/clients/typescript-client-example). ### Example Using OAuth2 Client Credentials @@ -66,7 +66,7 @@ let kpiData: KpiResult = await edcClient.useCaseApi.getKpis(); ## License Apache License 2.0 - see -[LICENSE](https://github.com/sovity/edc-extensions/blob/main/LICENSE) +[LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE) ## Contact diff --git a/extensions/wrapper/clients/typescript-client/package.json b/extensions/wrapper/clients/typescript-client/package.json index 905f3ea54..de71db7a0 100644 --- a/extensions/wrapper/clients/typescript-client/package.json +++ b/extensions/wrapper/clients/typescript-client/package.json @@ -7,13 +7,13 @@ "homepage": "https://sovity.de", "repository": { "type": "git", - "url": "https://github.com/sovity/edc-extensions/" + "url": "https://github.com/sovity/edc-ce/" }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "bugs": { - "url": "https://github.com/sovity/edc-extensions/issues/new/choose" + "url": "https://github.com/sovity/edc-ce/issues/new/choose" }, "keywords": [ "sovity", diff --git a/extensions/wrapper/wrapper-api/README.md b/extensions/wrapper/wrapper-api/README.md index f1dde77d4..8c5f00a47 100644 --- a/extensions/wrapper/wrapper-api/README.md +++ b/extensions/wrapper/wrapper-api/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Wrapper API Specification

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java index f04c04721..cb105b198 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java @@ -26,24 +26,24 @@ title = "sovity EDC API Wrapper", version = "0.0.0", description = "sovity's EDC API Wrapper contains a selection of APIs for multiple consumers, " + - "e.g. our EDC UI API, our generic Use Case API, our Commercial APIs, etc. " + + "e.g. our EDC UI API, our generic Use Case API, our Commercial Edition APIs, etc. " + "We bundled these APIs, so we can have an easier time generating our API Client Libraries.", contact = @Contact( - name = "Sovity GmbH", + name = "sovity GmbH", email = "contact@sovity.de", - url = "https://github.com/sovity/edc-extensions/issues/new/choose" + url = "https://github.com/sovity/edc-ce/issues/new/choose" ), license = @License( name = "Apache 2.0", - url = "https://github.com/sovity/edc-extensions/blob/main/LICENSE" + url = "https://github.com/sovity/edc-ce/blob/main/LICENSE" ) ), servers = { @Server(url = "https://my-connector/api/management") }, externalDocs = @ExternalDocumentation( - description = "EDC API Wrapper Project in sovity/edc-extensions", - url = "https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper" + description = "EDC API Wrapper Project in sovity/edc-ce", + url = "https://github.com/sovity/edc-ce/tree/main/extensions/wrapper" ) ) public interface ApiInformation { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 253c7dbbc..90904ca41 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -17,7 +17,7 @@ import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; @@ -74,7 +74,7 @@ interface UiResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Operation(description = "Updates an Asset's metadata") - IdResponseDto editAssetMetadata(@PathParam("assetId") String assetId, UiAssetEditMetadataRequest uiAssetEditMetadataRequest); + IdResponseDto editAssetMetadata(@PathParam("assetId") String assetId, UiAssetEditRequest uiAssetEditRequest); @DELETE @Path("pages/asset-page/assets/{assetId}") diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java index bcd792e22..80afa7fd1 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/AssetPage.java @@ -13,15 +13,21 @@ */ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "All data for the Asset Page") public class AssetPage { @Schema(description = "Visible Assets", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java index d4d543299..d2f8eee7c 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java @@ -14,23 +14,23 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.time.OffsetDateTime; import java.util.List; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Contract Agreement for Contract Agreement Page") public class ContractAgreementCard { @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java index 915ae73fc..4e112e45b 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPage.java @@ -14,14 +14,21 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Data as required by the UI's Contract Agreement Page") public class ContractAgreementPage { @Schema(description = "Contract Agreement Cards", requiredMode = Schema.RequiredMode.REQUIRED) private List contractAgreements; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java index 6c6dbcf6a..92e9577d6 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTransferProcess.java @@ -14,20 +14,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.time.OffsetDateTime; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "A Contract Agreement's Transfer Process") public class ContractAgreementTransferProcess { @Schema(description = "Transfer Process ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java index a19650dcf..25986ede7 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionEntry.java @@ -14,12 +14,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Contract Definition List Entry for Contract Definition Page") public class ContractDefinitionEntry { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java index 737fb2b36..1962eae7d 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionPage.java @@ -14,15 +14,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data @AllArgsConstructor -@Schema(description = "All data for the Contract Definition Page") +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class ContractDefinitionPage { @Schema(description = "Contract Definition Entries", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java index a4b603970..83afe19d3 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractDefinitionRequest.java @@ -15,20 +15,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.util.List; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Data for creating a Contract Definition") public class ContractDefinitionRequest { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java index 1ab5135be..fe2279644 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationRequest.java @@ -15,18 +15,18 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Data for initiating a Contract Negotiation") public class ContractNegotiationRequest { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java index b4aab9343..c5f770b52 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractNegotiationState.java @@ -14,18 +14,18 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Contract Negotiation State interpreted") public class ContractNegotiationState { @Schema(description = "State name or 'CUSTOM'. State names only exist for original EDC Contract Negotiation States.", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java index c6feb776f..a4cee19ac 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardDapsConfig.java @@ -1,12 +1,18 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; @Data -@NoArgsConstructor +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "DAPS Config") public class DashboardDapsConfig { @Schema(description = "Your Connector's DAPS Token URL", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java index 16b69fe61..78056c9a7 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardPage.java @@ -1,18 +1,19 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Data as required by the UI's Dashboard Page") public class DashboardPage { @Schema(description = "Number of Assets", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java index 476491e78..bbda22a13 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DashboardTransferAmounts.java @@ -1,18 +1,19 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Number of Transfer Processes for given direction.") public class DashboardTransferAmounts { @Schema(description = "Number of Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) private long numTotal; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java index 78b1f17a0..bf554c87b 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdResponseDto.java @@ -14,24 +14,26 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.time.OffsetDateTime; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Marks the operation as successful") public class IdResponseDto { @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) private final String id; + + @Builder.Default @Schema(description = "Change Date", requiredMode = Schema.RequiredMode.REQUIRED) private OffsetDateTime lastUpdatedDate = OffsetDateTime.now(); } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java index 5a6b87d81..4d596a20a 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateCustomTransferRequest.java @@ -14,18 +14,18 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Required data for starting a Contract Agreement's Transfer Process") public class InitiateCustomTransferRequest { @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java index 634ba0ddc..d6eed17f1 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/InitiateTransferRequest.java @@ -14,20 +14,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.util.Map; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "For type PARAMS_ONLY: Required data for starting a Transfer Process") public class InitiateTransferRequest { @Schema(description = "Contract Agreement ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java index 3791f2df5..285e338b9 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java @@ -14,15 +14,21 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "All data for the policy definition page as required by the UI", requiredMode = Schema.RequiredMode.REQUIRED) public class PolicyDefinitionPage { @Schema(description = "Policy Definition Entries", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java index 8de3d4160..4e29c6c68 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryEntry.java @@ -14,12 +14,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.time.OffsetDateTime; @Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Transfer History Entry for Transfer History Page") public class TransferHistoryEntry { @Schema(description = "Transfer Process ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java index cfe921976..e13809da0 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferHistoryPage.java @@ -14,14 +14,21 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Data as required by the UI's Transfer History Page") public class TransferHistoryPage { @Schema(description = "Transfer History Page Entries", requiredMode = Schema.RequiredMode.REQUIRED) private List transferEntries; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java index 210483a3d..fa87c6f84 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/TransferProcessState.java @@ -14,18 +14,18 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Transfer Process State interpreted") public class TransferProcessState { @Schema(description = "State name or 'CUSTOM'. State names only exist for original EDC Transfer Process States.", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java index 339425b1c..1235af25a 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractNegotiation.java @@ -15,20 +15,20 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.time.OffsetDateTime; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Contract Negotiation Information") public class UiContractNegotiation { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java index 9be8ba42d..8c7b42881 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiContractOffer.java @@ -15,11 +15,19 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; @Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Catalog Data Offer's Contract Offer as required by the UI") public class UiContractOffer { @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java index 465e4a300..80afa2bc9 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterion.java @@ -15,14 +15,18 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; @Data -@NoArgsConstructor @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Contract Definition Criterion as supported by the UI") public class UiCriterion { @Schema(description = "Left Operand", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java index 2209a072b..760d1ff45 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionLiteral.java @@ -3,19 +3,19 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.Builder; +import lombok.Data; import lombok.NonNull; -import lombok.ToString; +import lombok.RequiredArgsConstructor; import java.util.List; -@Getter -@ToString -@Schema(description = "Criterion Literal") -@NoArgsConstructor +@Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Criterion Literal") public class UiCriterionLiteral { private UiCriterionLiteralType type; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java index 205beb02b..9dfa624a9 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java @@ -14,16 +14,12 @@ package de.sovity.edc.ext.wrapper.api.ui.model; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.RequiredArgsConstructor; /** * Contract Definition Criterion - * - * @see
      org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl
      + * See
      org.eclipse.edc.connector.defaults.storage.CriterionToPredicateConverterImpl
      */ -@Getter -@RequiredArgsConstructor + @Schema(description = "Operator for constraints", enumAsRef = true) public enum UiCriterionOperator { EQ, diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java index 6f80a6b0b..16a8d6316 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiDataOffer.java @@ -14,13 +14,21 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.List; @Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Catalog Data Offer as required by the UI") public class UiDataOffer { @Schema(description = "Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java index 459ed44ed..218b337dd 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -14,11 +14,11 @@ package de.sovity.edc.ext.wrapper.api.usecase; -import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; +import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java index a1f0864a4..238d06e44 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpression.java @@ -14,15 +14,19 @@ package de.sovity.edc.ext.wrapper.api.usecase.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.utils.jsonld.vocab.Prop; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; @Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Generic expression for filtering the data offers in the catalog", requiredMode = Schema.RequiredMode.NOT_REQUIRED) public class CatalogFilterExpression { @Schema(description = "Asset property name", requiredMode = Schema.RequiredMode.REQUIRED, example = Prop.Edc.ASSET_ID) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java index e04a0ec42..8faa1d5d3 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionLiteral.java @@ -17,19 +17,19 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.Builder; +import lombok.Data; import lombok.NonNull; -import lombok.ToString; +import lombok.RequiredArgsConstructor; import java.util.List; -@Getter -@ToString -@Schema(description = "FilterExpression Criterion Literal") -@NoArgsConstructor +@Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "FilterExpression Criterion Literal") public class CatalogFilterExpressionLiteral { private CatalogFilterExpressionLiteralType type; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java index a098c3ae4..d19abab78 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogFilterExpressionOperator.java @@ -15,11 +15,7 @@ package de.sovity.edc.ext.wrapper.api.usecase.model; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -@Getter -@RequiredArgsConstructor @Schema(description = "Operator for filter expressions", enumAsRef = true) public enum CatalogFilterExpressionOperator { LIKE, EQ, IN diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java index 6ce7d90db..ed11ed655 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/CatalogQuery.java @@ -14,8 +14,10 @@ package de.sovity.edc.ext.wrapper.api.usecase.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -24,6 +26,8 @@ @Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Catalog query parameters") public class CatalogQuery { @Schema(description = "Target EDC DSP endpoint URL", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java index 847f67ee7..61a9a31f4 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/KpiResult.java @@ -14,18 +14,18 @@ package de.sovity.edc.ext.wrapper.api.usecase.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "EDC-status-defining KPIs") public class KpiResult { @Schema(description = "Counts of assets", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java index 3cd2d6ac3..da6b75996 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java @@ -14,21 +14,19 @@ package de.sovity.edc.ext.wrapper.api.usecase.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.common.model.PermissionDto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Policy Creation Request Supporting Multiplicity Constraints.") public class PolicyCreateRequest { diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java index 1059db3a5..366fab9ca 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/TransferProcessStatesDto.java @@ -14,15 +14,21 @@ package de.sovity.edc.ext.wrapper.api.usecase.model; +import com.fasterxml.jackson.annotation.JsonInclude; import de.sovity.edc.ext.wrapper.api.ui.model.TransferProcessSimplifiedState; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import java.util.Map; @Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class TransferProcessStatesDto { @Schema(description = "States and count of incoming transferprocess counts", requiredMode = Schema.RequiredMode.REQUIRED) private Map incomingTransferProcessCounts; diff --git a/extensions/wrapper/wrapper-common-api/README.md b/extensions/wrapper/wrapper-common-api/README.md index 8a83c802f..3194a827f 100644 --- a/extensions/wrapper/wrapper-common-api/README.md +++ b/extensions/wrapper/wrapper-common-api/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Common API Models

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java index 95741f6d5..32b28eafb 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java @@ -14,23 +14,21 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.time.OffsetDateTime; import java.util.Map; -@Getter -@Setter -@Builder(toBuilder = true) -@ToString +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Asset Details") public class AssetDto { @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java index 9ccdec720..560f93ce2 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java @@ -13,25 +13,19 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -/** - * Opinionated DTO of an EDC Constraint for permissions. - * - * @author tim.dahlmanns@isst.fraunhofer.de - */ -@Getter -@Setter -@Builder(toBuilder = true) -@ToString + +@Data @AllArgsConstructor @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Type-Safe OpenAPI generator friendly Constraint DTO that supports an opinionated" + " subset of the original EDC Constraint Entity.") diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java deleted file mode 100644 index 60437b6f6..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/CriterionDto.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - */ - -package de.sovity.edc.ext.wrapper.api.common.model; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder(toBuilder = true) -@NoArgsConstructor -@AllArgsConstructor -public class CriterionDto { - - @NotNull(message = "operandLeft cannot be null") - private Object operandLeft; - @NotNull(message = "operator cannot be null") - private String operator; - private Object operandRight; -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceAvailability.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceAvailability.java new file mode 100644 index 000000000..235c58cb7 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceAvailability.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + description = "Differentiate 'Live' Data Offers that have a real data source from " + + "'On Request' Data Offers that contain only a contact email address for requesting an individual data offer.", + enumAsRef = true +) +public enum DataSourceAvailability { + LIVE, + ON_REQUEST +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceType.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceType.java new file mode 100644 index 000000000..e486a14e5 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/DataSourceType.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Supported Data Source Types by UiDataSource", enumAsRef = true) +public enum DataSourceType { + HTTP_DATA, + ON_REQUEST, + CUSTOM +} + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java index 07e7adb65..9fc30e0de 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java @@ -14,22 +14,20 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.util.List; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Represents a single atomic constraint or a multiplicity constraint. The atomicConstraint" + " will be evaluated if the constraintType is ATOMIC_CONSTRAINT.") diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java index a29202dbe..5c107ab29 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java @@ -13,24 +13,20 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.Data; +import lombok.RequiredArgsConstructor; -/** - * Subset of the possible permissions in the EDC. - * - * @author tim.dahlmanns@isst.fraunhofer.de - */ -@Getter -@Setter + +@Data @AllArgsConstructor -@NoArgsConstructor +@RequiredArgsConstructor @Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class PermissionDto { @Schema(description = "Possible constraints for the permission", diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java index bc4d074f4..70392fff6 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java @@ -14,20 +14,18 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Data for creating a Policy Definition") public class PolicyDefinitionCreateRequest { @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java index b92164a0c..a0e917f2d 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java @@ -14,20 +14,18 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Policy Definition as required for the Policy Definition Page") public class PolicyDefinitionDto { @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java index 995b27bf4..bf28fc1a9 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java @@ -14,26 +14,25 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.time.LocalDate; import java.util.List; -@Getter -@Setter - -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Type-Safe Asset Metadata as needed by our UI") public class UiAsset { + @Schema(description = "'Live' vs 'On Request'", requiredMode = Schema.RequiredMode.REQUIRED) + private DataSourceAvailability dataSourceAvailability; @Schema(description = "Asset Id", requiredMode = Schema.RequiredMode.REQUIRED) private String assetId; @@ -50,6 +49,20 @@ public class UiAsset { @Schema(description = "Asset Organization Name", requiredMode = Schema.RequiredMode.REQUIRED) private String creatorOrganizationName; + @Schema( + description = "Contact E-Mail address. Only for dataSourceAvailability ON_REQUEST.", + example = "contact@my-org.com", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private String onRequestContactEmail; + + @Schema( + description = "Contact Preferred E-Mail Subject. Only for dataSourceAvailability ON_REQUEST.", + example = "Department XYZ Data Offer Request - My Product, My API", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private String onRequestContactEmailSubject; + @Schema(description = "Asset Language", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String language; @@ -80,16 +93,32 @@ public class UiAsset { @Schema(description = "Homepage URL associated with the Asset", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String landingPageUrl; - @Schema(description = "HTTP Datasource Hints Proxy Method", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Schema( + description = "HTTP Datasource Hint: Proxy Method. " + + "Only for dataSourceAvailability LIVE with an underlying HTTP_DATA Data Source.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) private Boolean httpDatasourceHintsProxyMethod; - @Schema(description = "HTTP Datasource Hints Proxy Path", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Schema( + description = "HTTP Datasource Hint: Proxy Path. " + + "Only for dataSourceAvailability LIVE with an underlying HTTP_DATA Data Source.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) private Boolean httpDatasourceHintsProxyPath; - @Schema(description = "HTTP Datasource Hints Proxy Query Params", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Schema( + description = "HTTP Datasource Hint: Proxy Query Params. " + + "Only for dataSourceAvailability LIVE with an underlying HTTP_DATA Data Source.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) private Boolean httpDatasourceHintsProxyQueryParams; - @Schema(description = "HTTP Datasource Hints Proxy Body", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Schema( + description = "HTTP Datasource Hint: Proxy Body. " + + "Only for dataSourceAvailability LIVE with an underlying HTTP_DATA Data Source.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) private Boolean httpDatasourceHintsProxyBody; @Schema(description = "Data Category", requiredMode = Schema.RequiredMode.NOT_REQUIRED) diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java index 53e6924d6..c49b7af1f 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java @@ -14,24 +14,26 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.Data; +import lombok.RequiredArgsConstructor; import java.time.LocalDate; import java.util.List; -import java.util.Map; -@Getter -@Setter -@Builder(toBuilder = true) -@NoArgsConstructor +@Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Type-Safe OpenAPI generator friendly Asset Create DTO that supports an opinionated subset of the original EDC Asset Entity.") public class UiAssetCreateRequest { + @Schema(description = "Data Source", requiredMode = Schema.RequiredMode.REQUIRED) + private UiDataSource dataSource; + @Schema(description = "Asset Id", requiredMode = Schema.RequiredMode.REQUIRED) private String id; @@ -107,26 +109,23 @@ public class UiAssetCreateRequest { @Schema(description = "Temporal coverage end date (inclusive)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private LocalDate temporalCoverageToInclusive; - @Schema(description = "Data Address", requiredMode = Schema.RequiredMode.REQUIRED) - private Map dataAddressProperties; - @Schema(description = "Contains serialized custom properties in the JSON format.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String customJsonAsString; @Schema(description = "Contains serialized custom properties in the JSON LD format. " + - "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + - "and will be affected by JSON LD compaction and expansion. " + - "Due to a technical limitation, the properties can't be booleans.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String customJsonLdAsString; @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String privateCustomJsonAsString; @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + - "The same limitations apply.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String privateCustomJsonLdAsString; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java similarity index 93% rename from extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java rename to extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java index 17c6c54f0..87257c694 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java @@ -14,23 +14,26 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.Data; +import lombok.RequiredArgsConstructor; import java.time.LocalDate; import java.util.List; -@Getter -@Setter -@Builder(toBuilder = true) -@NoArgsConstructor +@Data @AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Data for editing an asset.") -public class UiAssetEditMetadataRequest { +public class UiAssetEditRequest { + @Schema(description = "Data Source", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private UiDataSource dataSourceOverrideOrNull; + @Schema(description = "Asset Title", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String title; diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java new file mode 100644 index 000000000..bb57db00c --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Data Offer Data Source Model. Supports certain Data Address types but also leaves a backdoor for custom Data Address Properties.") +public class UiDataSource { + @Schema( + description = "Data Address Type.", + defaultValue = "CUSTOM", + requiredMode = Schema.RequiredMode.REQUIRED + ) + private DataSourceType type; + + @Schema( + description = "Only for type HTTP_DATA", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private UiDataSourceHttpData httpData; + + @Schema( + description = "Only for type ON_REQUEST", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private UiDataSourceOnRequest onRequest; + + @Schema( + description = "For all types. Custom Data Address Properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private Map customProperties; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java new file mode 100644 index 000000000..a44d9935e --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class UiDataSourceHttpData { + @Schema( + description = "HTTP Request Method", + defaultValue = "GET", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private UiDataSourceHttpDataMethod method; + + @Schema( + description = "HTTP Request URL. If parameterized, additional pathParams will be joined onto existing one.", + example = "https://my-app.my-org.com/api/edc-data-offer/v1", + requiredMode = Schema.RequiredMode.REQUIRED + ) + private String baseUrl; + + @Schema( + description = "HTTP Request Query Params / Query String.", + example = "search=example&limit=10", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private String queryString; + + @Schema( + description = "HTTP Request Headers. HTTP Header Parameterization is not available.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private Map headers; + + @Schema( + description = "Enable Method Parameterization. This forces consumers to provide" + + " a method, otherwise the transfer will fail.", + defaultValue = "false", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private Boolean enableMethodParameterization; + + @Schema( + description = "Enable Path Parameterization.", + defaultValue = "false", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private Boolean enablePathParameterization; + + @Schema( + description = "Enable Query Parameterization. Any additionally provided queryString" + + " will be merged with the existing one.", + defaultValue = "false", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private Boolean enableQueryParameterization; + + @Schema( + description = "Enable Body Parameterization. Forces the provider to provide both a" + + " request body and a content type. Only Methods POST, PUT and PATCH allow request bodies.", + defaultValue = "false", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private Boolean enableBodyParameterization; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java new file mode 100644 index 000000000..0244a47ab --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Supported HTTP Methods by UiDataSource", enumAsRef = true) +public enum UiDataSourceHttpDataMethod { + GET, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, +} + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceOnRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceOnRequest.java new file mode 100644 index 000000000..ce4402303 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceOnRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "ON_REQUEST type Data Source.") +public class UiDataSourceOnRequest { + @Schema( + description = "Contact E-Mail address", + example = "contact@my-org.com", + requiredMode = Schema.RequiredMode.REQUIRED + ) + private String contactEmail; + + @Schema( + description = "Contact Preferred E-Mail Subject", + example = "Department XYZ Data Offer Request - My Product, My API", + requiredMode = Schema.RequiredMode.REQUIRED + ) + private String contactPreferredEmailSubject; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java index ba307faa2..4118c7434 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java @@ -14,23 +14,21 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.util.List; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Type-Safe OpenAPI generator friendly Policy DTO as needed by our UI") public class UiPolicy { @Schema(description = "EDC Policy JSON-LD. This is required because the EDC requires the " + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java index 07c38e6fd..1df6cd1dc 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java @@ -14,21 +14,19 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "ODRL AtomicConstraint as supported by our UI") public class UiPolicyConstraint { @Schema(description = "Left side of the expression.", requiredMode = RequiredMode.REQUIRED) diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java index 769f615a2..b1f18465a 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java @@ -14,22 +14,20 @@ package de.sovity.edc.ext.wrapper.api.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.util.List; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Type-Safe OpenAPI generator friendly Policy Create DTO that supports an opinionated" + " subset of the original EDC Policy Entity.") public class UiPolicyCreateRequest { diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java index 355c8e1c3..7cb251703 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java @@ -19,22 +19,18 @@ import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; import java.util.ArrayList; import java.util.Collection; import java.util.List; -@Getter -@Setter -@ToString +@Data @AllArgsConstructor -@Builder(toBuilder = true) @RequiredArgsConstructor +@Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Sum type: A String, a list of Strings or a generic JSON value.") public class UiPolicyLiteral { diff --git a/extensions/wrapper/wrapper-common-mappers/README.md b/extensions/wrapper/wrapper-common-mappers/README.md index 15e50920e..5020b55b5 100644 --- a/extensions/wrapper/wrapper-common-mappers/README.md +++ b/extensions/wrapper/wrapper-common-mappers/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Common API Model Mappers

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java index 1c116d579..03c45f5af 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java @@ -1,13 +1,30 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.wrapper.api.common.mappers; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.utils.jsonld.JsonLdUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.Json; import jakarta.json.JsonObject; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.spi.types.domain.asset.Asset; @@ -19,49 +36,85 @@ @RequiredArgsConstructor public class AssetMapper { private final TypeTransformerRegistry typeTransformerRegistry; - private final UiAssetMapper uiAssetMapper; + private final AssetJsonLdBuilder assetJsonLdBuilder; + private final AssetJsonLdParser assetJsonLdParser; private final JsonLd jsonLd; - public UiAsset buildUiAsset(Asset asset, String connectorEndpoint, String participantId) { + public Asset buildAsset( + @NonNull UiAssetCreateRequest createRequest, + @NonNull String organizationName + ) { + var assetJsonLd = assetJsonLdBuilder.createAssetJsonLd(createRequest, organizationName); + return buildAsset(assetJsonLd); + } + + public Asset editAsset( + @NonNull Asset asset, + @NonNull UiAssetEditRequest editRequest + ) { var assetJsonLd = buildAssetJsonLd(asset); - return buildUiAsset(assetJsonLd, connectorEndpoint, participantId); + var editedJsonLd = assetJsonLdBuilder.editAssetJsonLd(assetJsonLd, editRequest); + return buildAsset(editedJsonLd); } - public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, String participantId) { - return uiAssetMapper.buildUiAsset(assetJsonLd, connectorEndpoint, participantId); + public UiAsset buildUiAsset( + @NonNull Asset asset, + @NonNull String connectorEndpoint, + @NonNull String participantId + ) { + var assetJsonLd = buildAssetJsonLd(asset); + return buildUiAsset(assetJsonLd, connectorEndpoint, participantId); } - public Asset buildAsset(UiAssetCreateRequest createRequest, String organizationName) { - var assetJsonLd = uiAssetMapper.buildAssetJsonLd(createRequest, organizationName); - return buildAsset(assetJsonLd); + public UiAsset buildUiAsset( + @NonNull JsonObject assetJsonLd, + @NonNull String connectorEndpoint, + @NonNull String participantId + ) { + return assetJsonLdParser.buildUiAsset(assetJsonLd, connectorEndpoint, participantId); } - public Asset buildAssetFromDatasetProperties(JsonObject json) { - return buildAsset(buildAssetJsonLdFromDatasetProperties(json)); + /** + * Maps "DCAT Dataset JSON-LD" to "EDC Asset JSON-LD" + * + * @param datasetJsonLd "DCAT Dataset JSON-LD" + * @return "EDC Asset JSON-LD" + */ + public Asset buildAssetFromDatasetProperties( + @NonNull JsonObject datasetJsonLd + ) { + var assetJsonLd = buildAssetJsonLdFromDatasetProperties(datasetJsonLd); + return buildAsset(assetJsonLd); } - public Asset buildAsset(JsonObject assetJsonLd) { + public Asset buildAsset( + @NonNull JsonObject assetJsonLd + ) { var expanded = jsonLd.expand(assetJsonLd) - .orElseThrow(FailedMappingException::ofFailure); + .orElseThrow(FailedMappingException::ofFailure); return typeTransformerRegistry.transform(expanded, Asset.class) - .orElseThrow(FailedMappingException::ofFailure); + .orElseThrow(FailedMappingException::ofFailure); } - private JsonObject buildAssetJsonLd(Asset asset) { + public JsonObject buildAssetJsonLd( + @NonNull Asset asset + ) { var assetJsonLd = typeTransformerRegistry.transform(asset, JsonObject.class) - .orElseThrow(FailedMappingException::ofFailure); + .orElseThrow(FailedMappingException::ofFailure); return jsonLd.expand(assetJsonLd) - .orElseThrow(FailedMappingException::ofFailure); + .orElseThrow(FailedMappingException::ofFailure); } - public JsonObject buildAssetJsonLdFromDatasetProperties(JsonObject json) { + public JsonObject buildAssetJsonLdFromDatasetProperties( + @NonNull JsonObject json + ) { // Try to use the EDC Prop ID, but if it's not available, fall back to the "@id" property var assetId = Optional.ofNullable(JsonLdUtils.string(json, Prop.Edc.ID)) - .orElseGet(() -> JsonLdUtils.string(json, Prop.ID)); + .orElseGet(() -> JsonLdUtils.string(json, Prop.ID)); return Json.createObjectBuilder() - .add(Prop.ID, assetId) - .add(Prop.Edc.PROPERTIES, json) - .build(); + .add(Prop.ID, assetId) + .add(Prop.Edc.PROPERTIES, json) + .build(); } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java index 81dc116f2..78f4fcb4d 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java @@ -1,10 +1,24 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.wrapper.api.common.mappers; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MappingErrors; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import de.sovity.edc.ext.wrapper.api.common.model.Expression; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java new file mode 100644 index 000000000..3ad1344e5 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + + +@RequiredArgsConstructor +public class AssetEditRequestMapper { + /** + * Builds a valid {@link UiAssetCreateRequest} from a {@link UiAssetEditRequest}. + *

      + * We do that to re-use the mapping for the edit process. + * + * @param editRequest {@link UiAssetEditRequest} + * @return {@link UiAssetCreateRequest} + */ + public UiAssetCreateRequest buildCreateRequest(@NonNull UiAssetEditRequest editRequest, @NonNull String assetId) { + var dataSource = editRequest.getDataSourceOverrideOrNull(); + if (dataSource == null) { + dataSource = dummyDataSource(); + } + + return UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(assetId) + .title(editRequest.getTitle()) + .language(editRequest.getLanguage()) + .description(editRequest.getDescription()) + .publisherHomepage(editRequest.getPublisherHomepage()) + .licenseUrl(editRequest.getLicenseUrl()) + .version(editRequest.getVersion()) + .keywords(editRequest.getKeywords()) + .mediaType(editRequest.getMediaType()) + .landingPageUrl(editRequest.getLandingPageUrl()) + .dataCategory(editRequest.getDataCategory()) + .dataSubcategory(editRequest.getDataSubcategory()) + .dataModel(editRequest.getDataModel()) + .geoReferenceMethod(editRequest.getGeoReferenceMethod()) + .transportMode(editRequest.getTransportMode()) + .sovereignLegalName(editRequest.getSovereignLegalName()) + .geoLocation(editRequest.getGeoLocation()) + .nutsLocations(editRequest.getNutsLocations()) + .dataSampleUrls(editRequest.getDataSampleUrls()) + .referenceFileUrls(editRequest.getReferenceFileUrls()) + .referenceFilesDescription(editRequest.getReferenceFilesDescription()) + .conditionsForUse(editRequest.getConditionsForUse()) + .dataUpdateFrequency(editRequest.getDataUpdateFrequency()) + .temporalCoverageFrom(editRequest.getTemporalCoverageFrom()) + .temporalCoverageToInclusive(editRequest.getTemporalCoverageToInclusive()) + .customJsonAsString(editRequest.getCustomJsonAsString()) + .customJsonLdAsString(editRequest.getCustomJsonLdAsString()) + .privateCustomJsonAsString(editRequest.getPrivateCustomJsonAsString()) + .privateCustomJsonLdAsString(editRequest.getPrivateCustomJsonLdAsString()) + .build(); + } + + private UiDataSource dummyDataSource() { + return UiDataSource.builder().type(DataSourceType.CUSTOM).build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java new file mode 100644 index 000000000..936ebaf1d --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset; + +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.val; +import org.apache.commons.collections4.CollectionUtils; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.apicatalog.jsonld.StringUtils.isBlank; +import static com.apicatalog.jsonld.StringUtils.isNotBlank; +import static de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.JsonBuilderUtils.addNonNull; +import static de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.JsonBuilderUtils.addNonNullJsonValue; +import static de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.JsonBuilderUtils.addNotBlank; +import static de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.JsonBuilderUtils.addNotBlankStringArray; + +@RequiredArgsConstructor +public class AssetJsonLdBuilder { + private final DataSourceMapper dataSourceMapper; + private final AssetJsonLdParser assetJsonLdParser; + private final AssetEditRequestMapper assetEditRequestMapper; + + + @SneakyThrows + @Nullable + public JsonObject createAssetJsonLd( + UiAssetCreateRequest createRequest, + String organizationName + ) { + var dataSourceJsonLd = dataSourceMapper.buildDataSourceJsonLd(createRequest.getDataSource()); + var properties = getAssetProperties(createRequest, dataSourceJsonLd, organizationName); + var privateProperties = getAssetPrivateProperties(createRequest); + + return buildAssetJsonLd( + createRequest.getId(), + properties, + privateProperties, + dataSourceJsonLd + ); + } + + + @SneakyThrows + @Nullable + public JsonObject editAssetJsonLd( + JsonObject assetJsonLd, + UiAssetEditRequest editRequest + ) { + var dataAddress = getDataAddressJsonLd(assetJsonLd); + if (editRequest.getDataSourceOverrideOrNull() != null) { + dataAddress = dataSourceMapper.buildDataSourceJsonLd(editRequest.getDataSourceOverrideOrNull()); + } + + var assetId = Objects.requireNonNull(JsonLdUtils.string(assetJsonLd, Prop.ID), "Asset JSON-LD had no @id"); + var organizationName = assetJsonLdParser.getCreatorOrganizationName(assetJsonLd); + var createRequest = assetEditRequestMapper.buildCreateRequest(editRequest, assetId); + var properties = getAssetProperties(createRequest, dataAddress, organizationName); + var privateProperties = getAssetPrivateProperties(createRequest); + + return buildAssetJsonLd( + assetId, + properties, + privateProperties, + dataAddress + ); + } + + private JsonObject buildAssetJsonLd( + String assetId, + JsonObject properties, + JsonObject privateProperties, + JsonObject dataSource + ) { + return Json.createObjectBuilder() + .add(Prop.ID, assetId) + .add(Prop.TYPE, Prop.Edc.TYPE_ASSET) + .add(Prop.Edc.PROPERTIES, properties) + .add(Prop.Edc.PRIVATE_PROPERTIES, privateProperties) + .add(Prop.Edc.DATA_ADDRESS, dataSource) + .build(); + } + + private JsonObject getAssetProperties( + UiAssetCreateRequest request, + JsonObject dataAddressJsonLd, + String organizationName + ) { + var properties = Json.createObjectBuilder(); + + addNotBlank(properties, Prop.Edc.ID, request.getId()); + addNotBlank(properties, Prop.Dcterms.LICENSE, request.getLicenseUrl()); + addNotBlank(properties, Prop.Dcterms.TITLE, request.getTitle()); + addNotBlank(properties, Prop.Dcterms.DESCRIPTION, request.getDescription()); + addNotBlank(properties, Prop.Dcterms.LANGUAGE, request.getLanguage()); + addNotBlank(properties, Prop.Dcat.VERSION, request.getVersion()); + addNotBlank(properties, Prop.Dcat.LANDING_PAGE, request.getLandingPageUrl()); + addNotBlank(properties, Prop.MobilityDcatAp.GEO_REFERENCE_METHOD, request.getGeoReferenceMethod()); + addNotBlank(properties, Prop.MobilityDcatAp.TRANSPORT_MODE, request.getTransportMode()); + addNotBlank(properties, Prop.Dcterms.RIGHTS_HOLDER, request.getSovereignLegalName()); + addNotBlank(properties, Prop.Dcterms.ACCRUAL_PERIODICITY, request.getDataUpdateFrequency()); + addNotBlankStringArray(properties, Prop.Dcat.KEYWORDS, request.getKeywords()); + addNonNull(properties, Prop.SovityDcatExt.CUSTOM_JSON, request.getCustomJsonAsString()); + + addPublisher(properties, request); + addCreator(properties, organizationName); + addDistribution(properties, request); + addTemporal(properties, request); + addSpatial(properties, request); + addMobilityTheme(properties, request); + + addCustomJsonLd(properties, request); + addDataSourceHints(properties, dataAddressJsonLd); + return properties.build(); + } + + private void addCreator(JsonObjectBuilder properties, String organizationName) { + properties.add(Prop.Dcterms.CREATOR, Json.createObjectBuilder() + .add(Prop.Foaf.NAME, organizationName)); + } + + private void addCustomJsonLd(JsonObjectBuilder properties, UiAssetCreateRequest request) { + var jsonLdStr = request.getCustomJsonLdAsString(); + if (jsonLdStr == null) { + return; + } + + var jsonLd = JsonUtils.parseJsonObj(jsonLdStr); + jsonLd.forEach((key, value) -> addNonNullJsonValue(properties, key, value)); + } + + private void addDataSourceHints(JsonObjectBuilder properties, JsonObject dataAddressJsonLd) { + var dataSourceHints = dataSourceMapper.buildAssetPropsFromDataAddress(dataAddressJsonLd); + properties.addAll(Json.createObjectBuilder(dataSourceHints)); + } + + private void addDistribution(JsonObjectBuilder properties, UiAssetCreateRequest request) { + var distribution = buildDistribution(request); + addNonNullJsonValue(properties, Prop.Dcat.DISTRIBUTION, distribution); + } + + private void addMobilityTheme(JsonObjectBuilder properties, UiAssetCreateRequest request) { + var dataCategory = request.getDataCategory(); + var dataSubcategory = request.getDataSubcategory(); + + if (isBlank(dataCategory) && isBlank(dataSubcategory)) { + return; + } + + var mobilityTheme = Json.createObjectBuilder(); + addNotBlank(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_CATEGORY, dataCategory); + addNotBlank(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_SUBCATEGORY, dataSubcategory); + properties.add(Prop.MobilityDcatAp.MOBILITY_THEME, mobilityTheme); + } + + private void addPublisher(JsonObjectBuilder properties, UiAssetCreateRequest request) { + var publisherHomepage = request.getPublisherHomepage(); + + if (isBlank(publisherHomepage)) { + return; + } + + var publisher = Json.createObjectBuilder().add(Prop.Foaf.HOMEPAGE, publisherHomepage); + properties.add(Prop.Dcterms.PUBLISHER, publisher); + } + + private void addSpatial(JsonObjectBuilder properties, UiAssetCreateRequest uiAssetCreateRequest) { + var nutsLocations = uiAssetCreateRequest.getNutsLocations(); + + if (isBlank(uiAssetCreateRequest.getGeoLocation()) && CollectionUtils.isEmpty(nutsLocations)) { + return; + } + + var spatial = Json.createObjectBuilder(); + addNotBlank(spatial, Prop.Skos.PREF_LABEL, uiAssetCreateRequest.getGeoLocation()); + addNotBlankStringArray(spatial, Prop.Dcterms.IDENTIFIER, uiAssetCreateRequest.getNutsLocations()); + properties.add(Prop.Dcterms.SPATIAL, spatial); + } + + private void addTemporal(JsonObjectBuilder properties, UiAssetCreateRequest request) { + var from = request.getTemporalCoverageFrom(); + var toInclusive = request.getTemporalCoverageToInclusive(); + + if (from == null && toInclusive == null) { + return; + } + + var temporal = Json.createObjectBuilder(); + addNonNull(temporal, Prop.Dcat.START_DATE, from); + addNonNull(temporal, Prop.Dcat.END_DATE, toInclusive); + properties.add(Prop.Dcterms.TEMPORAL, temporal); + } + + @Nullable + private JsonObject buildDistribution(UiAssetCreateRequest request) { + var dataSampleUrls = request.getDataSampleUrls(); + var referenceFileUrls = request.getReferenceFileUrls(); + + var hasRootLevel = request.getMediaType() != null || CollectionUtils.isNotEmpty(dataSampleUrls); + var hasRights = isNotBlank(request.getConditionsForUse()); + var hasDataModel = isNotBlank(request.getDataModel()); + var hasReferenceFiles = CollectionUtils.isNotEmpty(referenceFileUrls) + || isNotBlank(request.getReferenceFilesDescription()); + + if (!hasRootLevel && !hasRights && !hasDataModel && !hasReferenceFiles) { + return null; + } + + var distribution = Json.createObjectBuilder(); + addNotBlank(distribution, Prop.Dcat.MEDIATYPE, request.getMediaType()); + addNotBlankStringArray(distribution, Prop.Adms.SAMPLE, request.getDataSampleUrls()); + + if (hasRights) { + var rights = Json.createObjectBuilder(); + addNotBlank(rights, Prop.Rdfs.LABEL, request.getConditionsForUse()); + distribution.add(Prop.Dcterms.RIGHTS, rights); + } + + if (hasDataModel || hasReferenceFiles) { + var mobilityDataStandard = Json.createObjectBuilder(); + addNotBlank(mobilityDataStandard, Prop.ID, request.getDataModel()); + + if (hasReferenceFiles) { + var referenceFiles = Json.createObjectBuilder(); + addNotBlankStringArray(referenceFiles, Prop.Dcat.DOWNLOAD_URL, request.getReferenceFileUrls()); + addNotBlank(referenceFiles, Prop.Rdfs.LITERAL, request.getReferenceFilesDescription()); + mobilityDataStandard.add(Prop.MobilityDcatAp.SCHEMA, referenceFiles); + } + + distribution.add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, mobilityDataStandard); + } + + return distribution.build(); + } + + private JsonObject getAssetPrivateProperties(UiAssetCreateRequest uiAssetCreateRequest) { + var privateProperties = Json.createObjectBuilder(); + + val privateJsonStr = uiAssetCreateRequest.getPrivateCustomJsonAsString(); + if (privateJsonStr != null) { + addNonNull( + privateProperties, + Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON, + privateJsonStr + ); + } + + val privateJsonLdStr = uiAssetCreateRequest.getPrivateCustomJsonLdAsString(); + if (privateJsonLdStr != null) { + val privateJsonLd = JsonUtils.parseJsonObj(privateJsonLdStr); + privateJsonLd.forEach((k, v) -> addNonNullJsonValue(privateProperties, k, v)); + } + + return privateProperties.build(); + } + + private JsonObject getDataAddressJsonLd(JsonObject assetJsonLd) { + var dataAddress = JsonLdUtils.object(assetJsonLd, Prop.Edc.DATA_ADDRESS); + + if (!dataAddress.containsKey(Prop.Edc.PROPERTIES)) { + return dataAddress; + } + + return Json.createObjectBuilder(dataAddress) + .remove(Prop.Edc.PROPERTIES) + .addAll(Json.createObjectBuilder(JsonLdUtils.object(dataAddress, Prop.Edc.PROPERTIES))) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java new file mode 100644 index 000000000..84050b94b --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset; + +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceAvailability; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import de.sovity.edc.utils.jsonld.vocab.Prop.SovityDcatExt.HttpDatasourceHints; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import lombok.RequiredArgsConstructor; +import lombok.val; + +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +@RequiredArgsConstructor +public class AssetJsonLdParser { + private final AssetJsonLdUtils assetJsonLdUtils; + private final ShortDescriptionBuilder shortDescriptionBuilder; + private final OwnConnectorEndpointService ownConnectorEndpointService; + + public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, String participantId) { + var properties = JsonLdUtils.object(assetJsonLd, Prop.Edc.PROPERTIES); + + var uiAsset = new UiAsset(); + uiAsset.setAssetJsonLd(JsonUtils.toJson(JsonLdUtils.tryCompact(assetJsonLd))); + + var id = assetJsonLdUtils.getId(assetJsonLd); + var title = assetJsonLdUtils.getTitle(assetJsonLd); + + var distribution = JsonLdUtils.object(properties, Prop.Dcat.DISTRIBUTION); + uiAsset.setMediaType(JsonLdUtils.string(distribution, Prop.Dcat.MEDIATYPE)); + uiAsset.setDataSampleUrls(JsonLdUtils.stringList(distribution, Prop.Adms.SAMPLE)); + var rights = JsonLdUtils.object(distribution, Prop.Dcterms.RIGHTS); + uiAsset.setConditionsForUse(JsonLdUtils.string(rights, Prop.Rdfs.LABEL)); + var mobilityDataStandard = JsonLdUtils.object(distribution, Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); + uiAsset.setDataModel(JsonLdUtils.string(mobilityDataStandard, Prop.ID)); + var referenceFiles = JsonLdUtils.object(mobilityDataStandard, Prop.MobilityDcatAp.SCHEMA); + uiAsset.setReferenceFileUrls(JsonLdUtils.stringList(referenceFiles, Prop.Dcat.DOWNLOAD_URL)); + uiAsset.setReferenceFilesDescription(JsonLdUtils.string(referenceFiles, Prop.Rdfs.LITERAL)); + + + var temporalCoverage = JsonLdUtils.object(properties, Prop.Dcterms.TEMPORAL); + uiAsset.setTemporalCoverageFrom(JsonLdUtils.localDate(temporalCoverage, Prop.Dcat.START_DATE)); + uiAsset.setTemporalCoverageToInclusive(JsonLdUtils.localDate(temporalCoverage, Prop.Dcat.END_DATE)); + + var spatial = JsonLdUtils.object(properties, Prop.Dcterms.SPATIAL); + uiAsset.setGeoLocation(JsonLdUtils.string(spatial, Prop.Skos.PREF_LABEL)); + uiAsset.setNutsLocations(JsonLdUtils.stringList(spatial, Prop.Dcterms.IDENTIFIER)); + + var mobilityTheme = JsonLdUtils.object(properties, Prop.MobilityDcatAp.MOBILITY_THEME); + uiAsset.setDataCategory(JsonLdUtils.string(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_CATEGORY)); + uiAsset.setDataSubcategory(JsonLdUtils.string(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_SUBCATEGORY)); + + + var description = JsonLdUtils.string(properties, Prop.Dcterms.DESCRIPTION); + uiAsset.setDataSourceAvailability(getDataSourceAvailability(properties)); + uiAsset.setAssetId(id); + uiAsset.setConnectorEndpoint(connectorEndpoint); + uiAsset.setParticipantId(participantId); + uiAsset.setTitle(title); + uiAsset.setOnRequestContactEmail(JsonLdUtils.string(properties, Prop.SovityDcatExt.CONTACT_EMAIL)); + uiAsset.setOnRequestContactEmailSubject( + JsonLdUtils.string(properties, Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT) + ); + uiAsset.setLicenseUrl(JsonLdUtils.string(properties, Prop.Dcterms.LICENSE)); + uiAsset.setDescription(description); + uiAsset.setDescriptionShortText(shortDescriptionBuilder.buildShortDescription(description)); + uiAsset.setIsOwnConnector(ownConnectorEndpointService.isOwnConnectorEndpoint(connectorEndpoint)); + uiAsset.setLanguage(JsonLdUtils.string(properties, Prop.Dcterms.LANGUAGE)); + uiAsset.setVersion(JsonLdUtils.string(properties, Prop.Dcat.VERSION)); + uiAsset.setLandingPageUrl(JsonLdUtils.string(properties, Prop.Dcat.LANDING_PAGE)); + uiAsset.setGeoReferenceMethod(JsonLdUtils.string(properties, Prop.MobilityDcatAp.GEO_REFERENCE_METHOD)); + uiAsset.setTransportMode(JsonLdUtils.string(properties, Prop.MobilityDcatAp.TRANSPORT_MODE)); + uiAsset.setSovereignLegalName(JsonLdUtils.string(properties, Prop.Dcterms.RIGHTS_HOLDER)); + uiAsset.setDataUpdateFrequency(JsonLdUtils.string(properties, Prop.Dcterms.ACCRUAL_PERIODICITY)); + uiAsset.setKeywords(JsonLdUtils.stringList(properties, Prop.Dcat.KEYWORDS)); + + uiAsset.setHttpDatasourceHintsProxyMethod(JsonLdUtils.bool(properties, HttpDatasourceHints.METHOD)); + uiAsset.setHttpDatasourceHintsProxyPath(JsonLdUtils.bool(properties, HttpDatasourceHints.PATH)); + uiAsset.setHttpDatasourceHintsProxyQueryParams(JsonLdUtils.bool(properties, HttpDatasourceHints.QUERY_PARAMS)); + uiAsset.setHttpDatasourceHintsProxyBody(JsonLdUtils.bool(properties, HttpDatasourceHints.BODY)); + + var publisher = JsonLdUtils.object(properties, Prop.Dcterms.PUBLISHER); + uiAsset.setPublisherHomepage(JsonLdUtils.string(publisher, Prop.Foaf.HOMEPAGE)); + + uiAsset.setCustomJsonAsString(JsonLdUtils.string(properties, Prop.SovityDcatExt.CUSTOM_JSON)); + + uiAsset.setCreatorOrganizationName(ifBlank(getCreatorOrganizationName(assetJsonLd), participantId)); + + // Additional / Remaining Properties + uiAsset.setCustomJsonLdAsString(getCustomJsonLd(properties)); + + // Private Properties + val privateProperties = getPrivateProperties(assetJsonLd); + uiAsset.setPrivateCustomJsonAsString(JsonLdUtils.string(privateProperties, Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON)); + uiAsset.setPrivateCustomJsonLdAsString(getPrivateCustomJsonLd(privateProperties)); + + return uiAsset; + } + + private String getCustomJsonLd(JsonObject properties) { + // TODO: diff nested objects + val remaining = removeHandledProperties(properties, List.of( + // Implicitly handled / should be skipped if found + Prop.ID, + Prop.TYPE, + Prop.CONTEXT, + Prop.Edc.ID, + Prop.Dcterms.IDENTIFIER, + + // Explicitly handled + Prop.Dcat.DISTRIBUTION, + Prop.Dcat.KEYWORDS, + Prop.Dcat.LANDING_PAGE, + Prop.Dcat.VERSION, + Prop.Dcterms.CREATOR, + Prop.Dcterms.DESCRIPTION, + Prop.Dcterms.LANGUAGE, + Prop.Dcterms.LICENSE, + Prop.Dcterms.PUBLISHER, + Prop.Dcterms.TITLE, + Prop.MobilityDcatAp.GEO_REFERENCE_METHOD, + Prop.MobilityDcatAp.TRANSPORT_MODE, + Prop.Dcterms.TEMPORAL, + Prop.Dcterms.SPATIAL, + Prop.MobilityDcatAp.MOBILITY_THEME, + Prop.Dcterms.RIGHTS_HOLDER, + Prop.Dcterms.ACCRUAL_PERIODICITY, + + HttpDatasourceHints.BODY, + HttpDatasourceHints.METHOD, + HttpDatasourceHints.PATH, + HttpDatasourceHints.QUERY_PARAMS, + + Prop.SovityDcatExt.CUSTOM_JSON, + Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY, + Prop.SovityDcatExt.CONTACT_EMAIL, + Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT + )); + + // custom properties + return packAndSerializeJsonLd(remaining); + } + + private String getPrivateCustomJsonLd(JsonObject privateProperties) { + var remaining = removeHandledProperties(privateProperties, List.of( + Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON + )); + return packAndSerializeJsonLd(remaining); + } + + public String getCreatorOrganizationName(JsonObject assetJsonLd) { + var properties = JsonLdUtils.object(assetJsonLd, Prop.Edc.PROPERTIES); + var creator = JsonLdUtils.object(properties, Prop.Dcterms.CREATOR); + return JsonLdUtils.string(creator, Prop.Foaf.NAME); + } + + private String packAndSerializeJsonLd(JsonObject remaining) { + val customJsonLd = Json.createObjectBuilder(); + remaining.entrySet().stream() + .filter(it -> !JsonLdUtils.isEmptyArray(it.getValue()) || !JsonLdUtils.isEmptyObject(it.getValue())) + .forEach(it -> customJsonLd.add(it.getKey(), it.getValue())); + val compacted = JsonLdUtils.tryCompact(customJsonLd.build()); + return JsonUtils.toJson(compacted); + } + + + private JsonObject removeHandledProperties(JsonObject properties, List handledProperties) { + var remaining = Json.createObjectBuilder(JsonLdUtils.tryCompact(properties)); + handledProperties.forEach(remaining::remove); + return remaining.build(); + } + + private JsonObject getPrivateProperties(JsonObject assetJsonLd) { + if (assetJsonLd.containsKey(Prop.Edc.PRIVATE_PROPERTIES)) { + return JsonLdUtils.object(assetJsonLd, Prop.Edc.PRIVATE_PROPERTIES); + } else if (assetJsonLd.containsKey("privateProperties")) { + // Tests claim this path exists + return JsonLdUtils.object(assetJsonLd, "privateProperties"); + } else { + return JsonValue.EMPTY_JSON_OBJECT; + } + } + + private DataSourceAvailability getDataSourceAvailability(JsonObject properties) { + var typeValue = JsonLdUtils.string(properties, Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY); + if (Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY_ON_REQUEST.equalsIgnoreCase(typeValue)) { + return DataSourceAvailability.ON_REQUEST; + } + + return DataSourceAvailability.LIVE; + } + + private String ifBlank(String value, String defaultValue) { + return isBlank(value) ? defaultValue : value; + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/OwnConnectorEndpointService.java similarity index 88% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/OwnConnectorEndpointService.java index a60f61b2b..6a2f211ed 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/OwnConnectorEndpointService.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/OwnConnectorEndpointService.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.asset; @FunctionalInterface public interface OwnConnectorEndpointService { diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java similarity index 93% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java index 499e2b0ab..c0a018eba 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AssetJsonLdUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java @@ -11,7 +11,7 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; import de.sovity.edc.utils.jsonld.JsonLdUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java similarity index 93% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java index 5af531863..e7f198345 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java @@ -11,7 +11,7 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.types.domain.DataAddress; @@ -69,7 +69,7 @@ public Map toMapOfObject(Map map) { public DataAddress buildDataAddress(Map properties) { return DataAddress.Builder.newInstance() - .properties(toMapOfObject(properties)) - .build(); + .properties(toMapOfObject(properties)) + .build(); } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java similarity index 91% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java index 85c7b0e7f..f9063dc11 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/FailedMappingException.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java @@ -11,7 +11,7 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; import org.eclipse.edc.spi.result.Failure; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java new file mode 100644 index 000000000..a2158e37e --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; + +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonBuilderUtils { + + public static void addNonNull(JsonObjectBuilder builder, String key, String value) { + if (value != null) { + builder.add(key, value); + } + } + + public static void addNotBlank(JsonObjectBuilder builder, String key, String value) { + if (StringUtils.isNotBlank(value)) { + builder.add(key, value.trim()); + } + } + + public static void addNonNull(JsonObjectBuilder builder, String key, LocalDate value) { + if (value != null) { + builder.add(key, value.toString()); + } + } + + /** + * Adds non-null non-blank trimmed items as a JSON Array + * + * @param builder target object + * @param key key + * @param values list of values + */ + public static void addNotBlankStringArray(JsonObjectBuilder builder, String key, List values) { + var filteredItems = (values == null ? Stream.of() : values.stream()) + .filter(Objects::nonNull) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .toList(); + + if (CollectionUtils.isNotEmpty(filteredItems)) { + builder.add(key, Json.createArrayBuilder(filteredItems)); + } + } + + public static void addNonNullJsonValue(JsonObjectBuilder builder, String key, JsonValue value) { + if (value == null || value.getValueType() == JsonValue.ValueType.NULL) { + return; + } + + builder.add(key, value); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java similarity index 57% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java index 65b7e4b49..0d85792dd 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MarkdownToTextConverter.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java @@ -11,15 +11,25 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.data.MutableDataSet; import org.jsoup.Jsoup; -public class MarkdownToTextConverter { - public String extractText(String markdown) { +public class ShortDescriptionBuilder { + + public String buildShortDescription(String descriptionMarkdown) { + if (descriptionMarkdown == null) { + return null; + } + + var text = extractText(descriptionMarkdown); + return abbreviate(text, 300); + } + + String extractText(String markdown) { var options = new MutableDataSet(); var parser = Parser.builder(options).build(); var renderer = HtmlRenderer.builder(options).build(); @@ -27,4 +37,11 @@ public String extractText(String markdown) { var html = renderer.render(document); return Jsoup.parse(html).text(); } + + String abbreviate(String text, int maxCharacters) { + if (text == null) { + return null; + } + return text.substring(0, Math.min(maxCharacters, text.length())); + } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/DataSourceMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/DataSourceMapper.java new file mode 100644 index 000000000..b8ccae7ac --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/DataSourceMapper.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress; + +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceOnRequest; +import de.sovity.edc.utils.jsonld.JsonLdUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.stream.Collectors.toMap; + + +@RequiredArgsConstructor +public class DataSourceMapper { + private final EdcPropertyUtils edcPropertyUtils; + private final HttpDataSourceMapper httpDataSourceMapper; + + public JsonObject buildDataSourceJsonLd(@NonNull UiDataSource dataSource) { + var props = this.matchDataSource( + dataSource, + httpDataSourceMapper::buildDataAddress, + httpDataSourceMapper::buildOnRequestDataAddress, + dataSource::getCustomProperties + ); + + if (dataSource.getCustomProperties() != null) { + props.putAll(dataSource.getCustomProperties()); + } + + return buildDataAddressJsonLd(props); + } + + public JsonObject buildAssetPropsFromDataAddress(JsonObject dataAddressJsonLd) { + // We purposefully do not match the DataSource type but the properties to support the data address type "CUSTOM" + var dataAddress = parseDataAddressJsonLd(dataAddressJsonLd); + var type = dataAddress.getOrDefault(Prop.Edc.TYPE, ""); + + if (type.equals(Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA)) { + return httpDataSourceMapper.enhanceAssetWithDataSourceHints(dataAddress); + } + + return JsonValue.EMPTY_JSON_OBJECT; + } + + private T matchDataSource( + @NonNull UiDataSource dataSource, + @NonNull Function httpDataMapper, + @NonNull Function onRequestMapper, + @NonNull Supplier customMapper + ) { + var type = dataSource.getType(); + if (type == null) { + type = DataSourceType.CUSTOM; + } + + return switch (type) { + case HTTP_DATA -> httpDataMapper.apply(dataSource.getHttpData()); + case ON_REQUEST -> onRequestMapper.apply(dataSource.getOnRequest()); + case CUSTOM -> customMapper.get(); + }; + } + + private JsonObject buildDataAddressJsonLd(Map properties) { + var props = edcPropertyUtils.toMapOfObject(properties); + return Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .addAll(Json.createObjectBuilder(props)) + .build(); + } + + private Map parseDataAddressJsonLd(JsonObject dataAddressJsonLd) { + return dataAddressJsonLd.entrySet().stream() + .collect(toMap(Map.Entry::getKey, it -> { + var value = JsonLdUtils.string(it.getValue()); + return value == null ? "" : value; + })); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java new file mode 100644 index 000000000..ffb619fee --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http; + +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceOnRequest; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + + +@RequiredArgsConstructor +public class HttpDataSourceMapper { + private final HttpHeaderMapper httpHeaderMapper; + + /** + * Data Address for type HTTP_DATA + * + * @param httpData {@link UiDataSourceHttpData} + * @return properties for {@link org.eclipse.edc.spi.types.domain.DataAddress} + */ + public Map buildDataAddress(@NonNull UiDataSourceHttpData httpData) { + var baseUrl = requireNonNull(httpData.getBaseUrl(), "baseUrl must not be null"); + var props = new HashMap<>(Map.of( + Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA, + Prop.Edc.BASE_URL, baseUrl + )); + + if (httpData.getMethod() != null) { + props.put(Prop.Edc.METHOD, httpData.getMethod().name()); + } + + if (StringUtils.isNotBlank(httpData.getQueryString())) { + props.put(Prop.Edc.QUERY_PARAMS, httpData.getQueryString()); + } + + props.putAll(httpHeaderMapper.buildHeaderProps(httpData.getHeaders())); + + // Parameterization + if (Boolean.TRUE.equals(httpData.getEnableMethodParameterization())) { + props.put(Prop.Edc.PROXY_METHOD, "true"); + } + if (Boolean.TRUE.equals(httpData.getEnablePathParameterization())) { + props.put(Prop.Edc.PROXY_PATH, "true"); + } + if (Boolean.TRUE.equals(httpData.getEnableQueryParameterization())) { + props.put(Prop.Edc.PROXY_QUERY_PARAMS, "true"); + } + if (Boolean.TRUE.equals(httpData.getEnableBodyParameterization())) { + props.put(Prop.Edc.PROXY_BODY, "true"); + } + + return props; + } + + public Map buildOnRequestDataAddress(@NonNull UiDataSourceOnRequest onRequest) { + var contactEmail = requireNonNull(onRequest.getContactEmail(), "contactEmail must not be null"); + var contactEmailSubject = requireNonNull( + onRequest.getContactPreferredEmailSubject(), + "Need contactPreferredEmailSubject" + ); + + var actualDataSource = UiDataSourceHttpData.builder() + .baseUrl("http://0.0.0.0") + .build(); + + var props = buildDataAddress(actualDataSource); + props.put(Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY, Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY_ON_REQUEST); + props.put(Prop.SovityDcatExt.CONTACT_EMAIL, contactEmail); + props.put(Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT, contactEmailSubject); + return props; + } + + /** + * Public information from Data Address + * + * @param dataAddress data address + * @return json object to be merged with asset properties + */ + public JsonObject enhanceAssetWithDataSourceHints(Map dataAddress) { + var json = Json.createObjectBuilder(); + + // Parameterization Hints + var isOnRequest = Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY_ON_REQUEST + .equals(dataAddress.get(Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY)); + if (!isOnRequest) { + Map.of( + Prop.Edc.PROXY_METHOD, Prop.SovityDcatExt.HttpDatasourceHints.METHOD, + Prop.Edc.PROXY_PATH, Prop.SovityDcatExt.HttpDatasourceHints.PATH, + Prop.Edc.PROXY_QUERY_PARAMS, Prop.SovityDcatExt.HttpDatasourceHints.QUERY_PARAMS, + Prop.Edc.PROXY_BODY, Prop.SovityDcatExt.HttpDatasourceHints.BODY + ).forEach((prop, hint) -> + // Will add hints as "true" or "false" + json.add(hint, String.valueOf("true".equals(dataAddress.get(prop)))) + ); + } + + // On Request information + Set.of( + Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY, + Prop.SovityDcatExt.CONTACT_EMAIL, + Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT + ).forEach(prop -> { + var value = dataAddress.get(prop); + if (StringUtils.isNotBlank(value)) { + json.add(prop, value); + } + }); + + return json.build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java new file mode 100644 index 000000000..b679687df --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import static java.util.stream.Collectors.toMap; + + +@RequiredArgsConstructor +public class HttpHeaderMapper { + public Map buildHeaderProps(@Nullable Map headers) { + return mapKeys( + headers, + key -> { + if ("content-type".equalsIgnoreCase(key)) { + // Content-Type is overridden by a special Data Address property + // So we should set that instead of attempting to set a header + return Prop.Edc.CONTENT_TYPE; + } else { + return "header:%s".formatted(key); + } + } + ); + } + + private Map mapKeys( + @Nullable Map map, + @NonNull Function keyMapper + ) { + if (map == null) { + return Collections.emptyMap(); + } + + return map.entrySet().stream().collect(toMap( + e -> keyMapper.apply(e.getKey()), + Map.Entry::getValue, + (v1, v2) -> v1 // lenient merge function + )); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java similarity index 96% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java index 896fc56e5..47c75f218 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java @@ -11,9 +11,8 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.model.AtomicConstraintDto; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java similarity index 98% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java index 5ea4ab12e..1ac5f3ca1 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractor.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java @@ -11,7 +11,7 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import lombok.RequiredArgsConstructor; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java similarity index 96% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java index 2bf607606..92f0194bc 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java @@ -11,9 +11,10 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java similarity index 95% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java index e6894173a..ad58bde83 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrors.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java @@ -11,7 +11,7 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import lombok.Getter; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapper.java similarity index 93% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapper.java index 19d992a48..8dcae37a2 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapper.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.wrapper.api.common.mappers; +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import lombok.RequiredArgsConstructor; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java similarity index 96% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java rename to extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java index 131470fcb..7bdc44502 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidator.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java @@ -11,8 +11,9 @@ * sovity GmbH - init */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java deleted file mode 100644 index 5604b2dfe..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - */ - -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - -import de.sovity.edc.utils.JsonUtils; -import jakarta.json.Json; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonValue; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.commons.collections4.CollectionUtils; - -import java.time.LocalDate; -import java.util.List; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class JsonBuilderUtils { - - protected static JsonObjectBuilder addNonNull(JsonObjectBuilder builder, String key, String value) { - if (value != null) { - builder.add(key, value); - } - return builder; - } - - protected static JsonObjectBuilder addNotBlank(JsonObjectBuilder builder, String key, String value) { - if (value != null && !value.trim().isBlank()) { - builder.add(key, value); - } - return builder; - } - - protected static JsonObjectBuilder addNonNull(JsonObjectBuilder builder, String key, LocalDate value) { - if (value != null) { - builder.add(key, value.toString()); - } - return builder; - } - - protected static JsonObjectBuilder addNonNullArray(JsonObjectBuilder builder, String key, List values) { - if (CollectionUtils.isNotEmpty(values)) { - builder.add(key, Json.createArrayBuilder(values)); - } - return builder; - } - - protected static JsonObjectBuilder addNonNullJsonValue(JsonObjectBuilder builder, String key, String jsonString) { - if (jsonString == null) { - return builder; - } - var value = JsonUtils.parseJsonValue(jsonString); - if (value.getValueType() == JsonValue.ValueType.NULL) { - return builder; - } - - builder.add(key, value); - return builder; - } - - protected static JsonObjectBuilder addNonNullJsonValue(JsonObjectBuilder builder, String key, JsonValue value) { - if (value == null || value.getValueType() == JsonValue.ValueType.NULL) { - return builder; - } - - builder.add(key, value); - return builder; - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java deleted file mode 100644 index 5de523e29..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - */ - -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - -public class TextUtils { - public String abbreviate(String text, int maxCharacters) { - if (text == null) { - return null; - } - return text.substring(0, Math.min(maxCharacters, text.length())); - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java deleted file mode 100644 index 523acd351..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - */ - -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - -import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.jsonld.JsonLdUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import de.sovity.edc.utils.jsonld.vocab.Prop.SovityDcatExt.HttpDatasourceHints; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonValue; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.val; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNonNull; -import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNonNullArray; -import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNonNullJsonValue; -import static de.sovity.edc.ext.wrapper.api.common.mappers.utils.JsonBuilderUtils.addNotBlank; -import static org.apache.commons.lang3.StringUtils.isBlank; - -@RequiredArgsConstructor -public class UiAssetMapper { - private final EdcPropertyUtils edcPropertyUtils; - private final AssetJsonLdUtils assetJsonLdUtils; - private final MarkdownToTextConverter markdownToTextConverter; - private final TextUtils textUtils; - private final OwnConnectorEndpointService ownConnectorEndpointService; - - public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, String participantId) { - var properties = JsonLdUtils.object(assetJsonLd, Prop.Edc.PROPERTIES); - - var uiAsset = new UiAsset(); - uiAsset.setAssetJsonLd(JsonUtils.toJson(JsonLdUtils.tryCompact(assetJsonLd))); - - var id = assetJsonLdUtils.getId(assetJsonLd); - var title = assetJsonLdUtils.getTitle(assetJsonLd); - - var distribution = JsonLdUtils.object(properties, Prop.Dcat.DISTRIBUTION); - uiAsset.setMediaType(JsonLdUtils.string(distribution, Prop.Dcat.MEDIATYPE)); - uiAsset.setDataSampleUrls(JsonLdUtils.stringList(distribution, Prop.Adms.SAMPLE)); - var rights = JsonLdUtils.object(distribution, Prop.Dcterms.RIGHTS); - uiAsset.setConditionsForUse(JsonLdUtils.string(rights, Prop.Rdfs.LABEL)); - var mobilityDataStandard = JsonLdUtils.object(distribution, Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); - uiAsset.setDataModel(JsonLdUtils.string(mobilityDataStandard, Prop.ID)); - var referenceFiles = JsonLdUtils.object(mobilityDataStandard, Prop.MobilityDcatAp.SCHEMA); - uiAsset.setReferenceFileUrls(JsonLdUtils.stringList(referenceFiles, Prop.Dcat.DOWNLOAD_URL)); - uiAsset.setReferenceFilesDescription(JsonLdUtils.string(referenceFiles, Prop.Rdfs.LITERAL)); - - - var temporalCoverage = JsonLdUtils.object(properties, Prop.Dcterms.TEMPORAL); - uiAsset.setTemporalCoverageFrom(JsonLdUtils.localDate(temporalCoverage, Prop.Dcat.START_DATE)); - uiAsset.setTemporalCoverageToInclusive(JsonLdUtils.localDate(temporalCoverage, Prop.Dcat.END_DATE)); - - var spatial = JsonLdUtils.object(properties, Prop.Dcterms.SPATIAL); - uiAsset.setGeoLocation(JsonLdUtils.string(spatial, Prop.Skos.PREF_LABEL)); - uiAsset.setNutsLocations(JsonLdUtils.stringList(spatial, Prop.Dcterms.IDENTIFIER)); - - var mobilityTheme = JsonLdUtils.object(properties, Prop.MobilityDcatAp.MOBILITY_THEME); - uiAsset.setDataCategory(JsonLdUtils.string(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_CATEGORY)); - uiAsset.setDataSubcategory(JsonLdUtils.string(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_SUBCATEGORY)); - - var creator = JsonLdUtils.object(properties, Prop.Dcterms.CREATOR); - var creatorOrganizationName = JsonLdUtils.string(creator, Prop.Foaf.NAME); - creatorOrganizationName = isBlank(creatorOrganizationName) ? participantId : creatorOrganizationName; - - - var description = JsonLdUtils.string(properties, Prop.Dcterms.DESCRIPTION); - uiAsset.setAssetId(id); - uiAsset.setConnectorEndpoint(connectorEndpoint); - uiAsset.setParticipantId(participantId); - uiAsset.setTitle(title); - uiAsset.setLicenseUrl(JsonLdUtils.string(properties, Prop.Dcterms.LICENSE)); - uiAsset.setDescription(description); - uiAsset.setDescriptionShortText(buildShortDescription(description)); - uiAsset.setIsOwnConnector(ownConnectorEndpointService.isOwnConnectorEndpoint(connectorEndpoint)); - uiAsset.setLanguage(JsonLdUtils.string(properties, Prop.Dcterms.LANGUAGE)); - uiAsset.setVersion(JsonLdUtils.string(properties, Prop.Dcat.VERSION)); - uiAsset.setLandingPageUrl(JsonLdUtils.string(properties, Prop.Dcat.LANDING_PAGE)); - uiAsset.setGeoReferenceMethod(JsonLdUtils.string(properties, Prop.MobilityDcatAp.GEO_REFERENCE_METHOD)); - uiAsset.setTransportMode(JsonLdUtils.string(properties, Prop.MobilityDcatAp.TRANSPORT_MODE)); - uiAsset.setSovereignLegalName(JsonLdUtils.string(properties, Prop.Dcterms.RIGHTS_HOLDER)); - uiAsset.setDataUpdateFrequency(JsonLdUtils.string(properties, Prop.Dcterms.ACCRUAL_PERIODICITY)); - uiAsset.setKeywords(JsonLdUtils.stringList(properties, Prop.Dcat.KEYWORDS)); - - uiAsset.setHttpDatasourceHintsProxyMethod(JsonLdUtils.bool(properties, HttpDatasourceHints.METHOD)); - uiAsset.setHttpDatasourceHintsProxyPath(JsonLdUtils.bool(properties, HttpDatasourceHints.PATH)); - uiAsset.setHttpDatasourceHintsProxyQueryParams(JsonLdUtils.bool(properties, HttpDatasourceHints.QUERY_PARAMS)); - uiAsset.setHttpDatasourceHintsProxyBody(JsonLdUtils.bool(properties, HttpDatasourceHints.BODY)); - - var publisher = JsonLdUtils.object(properties, Prop.Dcterms.PUBLISHER); - uiAsset.setPublisherHomepage(JsonLdUtils.string(publisher, Prop.Foaf.HOMEPAGE)); - - uiAsset.setCustomJsonAsString(JsonLdUtils.string(properties, Prop.SovityDcatExt.CUSTOM_JSON)); - - uiAsset.setCreatorOrganizationName(creatorOrganizationName); - - // Additional / Remaining Properties - // TODO: diff nested objects - val remaining = removeHandledProperties(properties, List.of( - // Implicitly handled / should be skipped if found - Prop.ID, - Prop.TYPE, - Prop.CONTEXT, - Prop.Edc.ID, - Prop.Dcterms.IDENTIFIER, - - // Explicitly handled - Prop.Dcat.DISTRIBUTION, - Prop.Dcat.KEYWORDS, - Prop.Dcat.LANDING_PAGE, - Prop.Dcat.VERSION, - Prop.Dcterms.CREATOR, - Prop.Dcterms.DESCRIPTION, - Prop.Dcterms.LANGUAGE, - Prop.Dcterms.LICENSE, - Prop.Dcterms.PUBLISHER, - Prop.Dcterms.TITLE, - Prop.MobilityDcatAp.GEO_REFERENCE_METHOD, - Prop.MobilityDcatAp.TRANSPORT_MODE, - Prop.Dcterms.TEMPORAL, - Prop.Dcterms.SPATIAL, - Prop.MobilityDcatAp.MOBILITY_THEME, - Prop.Dcterms.RIGHTS_HOLDER, - Prop.Dcterms.ACCRUAL_PERIODICITY, - - HttpDatasourceHints.BODY, - HttpDatasourceHints.METHOD, - HttpDatasourceHints.PATH, - HttpDatasourceHints.QUERY_PARAMS, - - Prop.SovityDcatExt.CUSTOM_JSON - )); - - // custom properties - val serializedJsonLd = packAsJsonLdProperties(remaining); - uiAsset.setCustomJsonLdAsString(serializedJsonLd); - - // Private Properties - val privateProperties = getPrivateProperties(assetJsonLd); - if (privateProperties != null) { - val privateCustomJson = JsonLdUtils.string(privateProperties, Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON); - uiAsset.setPrivateCustomJsonAsString(privateCustomJson); - - val privateRemaining = removeHandledProperties( - privateProperties, - List.of(Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON)); - val privateSerializedJsonLd = packAsJsonLdProperties(privateRemaining); - uiAsset.setPrivateCustomJsonLdAsString(privateSerializedJsonLd); - } - - return uiAsset; - } - - private static String packAsJsonLdProperties(JsonObject remaining) { - val customJsonLd = Json.createObjectBuilder(); - remaining.entrySet().stream() - .filter(it -> !JsonLdUtils.isEmptyArray(it.getValue()) || !JsonLdUtils.isEmptyObject(it.getValue())) - .forEach(it -> customJsonLd.add(it.getKey(), it.getValue())); - val compacted = JsonLdUtils.tryCompact(customJsonLd.build()); - return JsonUtils.toJson(compacted); - } - - @SneakyThrows - @Nullable - public JsonObject buildAssetJsonLd( - UiAssetCreateRequest uiAssetCreateRequest, - String organizationName - ) { - var properties = getAssetProperties(uiAssetCreateRequest, organizationName); - var privateProperties = getAssetPrivateProperties(uiAssetCreateRequest); - var dataAddress = getDataAddress(uiAssetCreateRequest); - - return Json.createObjectBuilder() - .add(Prop.ID, uiAssetCreateRequest.getId()) - .add(Prop.TYPE, Prop.Edc.TYPE_ASSET) - .add(Prop.Edc.PROPERTIES, properties) - .add(Prop.Edc.PRIVATE_PROPERTIES, privateProperties) - .add(Prop.Edc.DATA_ADDRESS, dataAddress) - .build(); - } - - private JsonObjectBuilder getAssetProperties( - UiAssetCreateRequest uiAssetCreateRequest, - String organizationName - ) { - var properties = Json.createObjectBuilder(); - - addNonNull(properties, Prop.Edc.ID, uiAssetCreateRequest.getId()); - addNonNull(properties, Prop.Dcterms.LICENSE, uiAssetCreateRequest.getLicenseUrl()); - addNonNull(properties, Prop.Dcterms.TITLE, uiAssetCreateRequest.getTitle()); - addNonNull(properties, Prop.Dcterms.DESCRIPTION, uiAssetCreateRequest.getDescription()); - addNonNull(properties, Prop.Dcterms.LANGUAGE, uiAssetCreateRequest.getLanguage()); - addNonNull(properties, Prop.Dcat.VERSION, uiAssetCreateRequest.getVersion()); - addNonNull(properties, Prop.Dcat.LANDING_PAGE, uiAssetCreateRequest.getLandingPageUrl()); - addNonNull(properties, Prop.MobilityDcatAp.GEO_REFERENCE_METHOD, uiAssetCreateRequest.getGeoReferenceMethod()); - addNonNull(properties, Prop.MobilityDcatAp.TRANSPORT_MODE, uiAssetCreateRequest.getTransportMode()); - addNonNull(properties, Prop.Dcterms.RIGHTS_HOLDER, uiAssetCreateRequest.getSovereignLegalName()); - addNonNull(properties, Prop.Dcterms.ACCRUAL_PERIODICITY, uiAssetCreateRequest.getDataUpdateFrequency()); - - addNonNullArray(properties, Prop.Dcat.KEYWORDS, uiAssetCreateRequest.getKeywords()); - - if (uiAssetCreateRequest.getPublisherHomepage() != null) { - properties.add(Prop.Dcterms.PUBLISHER, Json.createObjectBuilder() - .add(Prop.Foaf.HOMEPAGE, uiAssetCreateRequest.getPublisherHomepage())); - } - - properties.add(Prop.Dcterms.CREATOR, Json.createObjectBuilder() - .add(Prop.Foaf.NAME, organizationName)); - - var distribution = buildDistribution(uiAssetCreateRequest); - if (distribution != null) { - properties.add(Prop.Dcat.DISTRIBUTION, distribution); - } - - if (uiAssetCreateRequest.getTemporalCoverageFrom() != null || uiAssetCreateRequest.getTemporalCoverageToInclusive() != null) { - var temporal = Json.createObjectBuilder(); - addNonNull(temporal, Prop.Dcat.START_DATE, uiAssetCreateRequest.getTemporalCoverageFrom()); - addNonNull(temporal, Prop.Dcat.END_DATE, uiAssetCreateRequest.getTemporalCoverageToInclusive()); - properties.add(Prop.Dcterms.TEMPORAL, temporal); - } - - var nutsLocations = uiAssetCreateRequest.getNutsLocations(); - if (uiAssetCreateRequest.getGeoLocation() != null || (nutsLocations != null && !nutsLocations.isEmpty())) { - var spatial = Json.createObjectBuilder(); - addNonNull(spatial, Prop.Skos.PREF_LABEL, uiAssetCreateRequest.getGeoLocation()); - addNonNullArray(spatial, Prop.Dcterms.IDENTIFIER, uiAssetCreateRequest.getNutsLocations()); - properties.add(Prop.Dcterms.SPATIAL, spatial); - } - - if (uiAssetCreateRequest.getDataCategory() != null || uiAssetCreateRequest.getDataSubcategory() != null) { - var mobilityTheme = Json.createObjectBuilder(); - addNonNull(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_CATEGORY, uiAssetCreateRequest.getDataCategory()); - addNonNull(mobilityTheme, Prop.MobilityDcatAp.DataCategoryProps.DATA_SUBCATEGORY, uiAssetCreateRequest.getDataSubcategory()); - properties.add(Prop.MobilityDcatAp.MOBILITY_THEME, mobilityTheme); - } - - var dataAddress = uiAssetCreateRequest.getDataAddressProperties(); - if (dataAddress != null && dataAddress.get(Prop.Edc.TYPE).equals("HttpData")) { - addNonNull(properties, HttpDatasourceHints.BODY, trueIfTrue(dataAddress, Prop.Edc.PROXY_BODY)); - addNonNull(properties, HttpDatasourceHints.PATH, trueIfTrue(dataAddress, Prop.Edc.PROXY_PATH)); - addNonNull(properties, HttpDatasourceHints.QUERY_PARAMS, trueIfTrue(dataAddress, Prop.Edc.PROXY_QUERY_PARAMS)); - addNonNull(properties, HttpDatasourceHints.METHOD, trueIfTrue(dataAddress, Prop.Edc.PROXY_METHOD)); - } - - addNonNull(properties, Prop.SovityDcatExt.CUSTOM_JSON, uiAssetCreateRequest.getCustomJsonAsString()); - val jsonLdStr = uiAssetCreateRequest.getCustomJsonLdAsString(); - if (jsonLdStr != null) { - val jsonLd = JsonUtils.parseJsonObj(jsonLdStr); - for (val e : jsonLd.entrySet()) { - addNonNullJsonValue(properties, e.getKey(), e.getValue()); - } - } - - return properties; - } - - private JsonObjectBuilder getAssetPrivateProperties(UiAssetCreateRequest uiAssetCreateRequest) { - var privateProperties = Json.createObjectBuilder(); - - val privateJsonStr = uiAssetCreateRequest.getPrivateCustomJsonAsString(); - if (privateJsonStr != null) { - addNonNull( - privateProperties, - Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON, - privateJsonStr - ); - } - - val privateJsonLdStr = uiAssetCreateRequest.getPrivateCustomJsonLdAsString(); - if (privateJsonLdStr != null) { - val privateJsonLd = JsonUtils.parseJsonObj(privateJsonLdStr); - privateJsonLd.forEach((k, v) -> addNonNullJsonValue(privateProperties, k, v)); - } - - return privateProperties; - } - - private String trueIfTrue(Map dataAddressProperties, String key) { - return "true".equals(dataAddressProperties.get(key)) ? "true" : "false"; - } - - private JsonObjectBuilder getDataAddress(UiAssetCreateRequest uiAssetCreateRequest) { - var props = edcPropertyUtils.toMapOfObject(uiAssetCreateRequest.getDataAddressProperties()); - return Json.createObjectBuilder() - .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) - .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder(props)); - } - - private JsonObject removeHandledProperties(JsonObject properties, List handledProperties) { - var remaining = Json.createObjectBuilder(JsonLdUtils.tryCompact(properties)); - handledProperties.forEach(remaining::remove); - return remaining.build(); - } - - private JsonObject getPrivateProperties(JsonObject assetJsonLd) { - if (assetJsonLd.containsKey(Prop.Edc.PRIVATE_PROPERTIES)) { - return JsonLdUtils.object(assetJsonLd, Prop.Edc.PRIVATE_PROPERTIES); - } else if (assetJsonLd.containsKey("privateProperties")) { - // Tests claim this path exists - return JsonLdUtils.object(assetJsonLd, "privateProperties"); - } else { - return JsonValue.EMPTY_JSON_OBJECT; - } - } - - private String buildShortDescription(String description) { - if (description == null) { - return null; - } - - var text = markdownToTextConverter.extractText(description); - return textUtils.abbreviate(text, 300); - } - - private JsonObjectBuilder buildDistribution(UiAssetCreateRequest uiAssetCreateRequest) { - var dataSampleUrls = uiAssetCreateRequest.getDataSampleUrls(); - var referenceFileUrls = uiAssetCreateRequest.getReferenceFileUrls(); - var hasRootLevelFields = uiAssetCreateRequest.getMediaType() != null - || (dataSampleUrls != null && !dataSampleUrls.isEmpty()); - var hasRightsFields = uiAssetCreateRequest.getConditionsForUse() != null; - var hasDataModelFields = uiAssetCreateRequest.getDataModel() != null - && !uiAssetCreateRequest.getDataModel().isBlank(); - var hasReferenceFilesFields = (referenceFileUrls != null && !referenceFileUrls.isEmpty()) - || uiAssetCreateRequest.getReferenceFilesDescription() != null; - - if (!hasRootLevelFields && !hasRightsFields && !hasDataModelFields && !hasReferenceFilesFields) { - return null; - } - - var distribution = Json.createObjectBuilder(); - addNonNull(distribution, Prop.Dcat.MEDIATYPE, uiAssetCreateRequest.getMediaType()); - addNonNullArray(distribution, Prop.Adms.SAMPLE, uiAssetCreateRequest.getDataSampleUrls()); - - if (hasRightsFields) { - var rights = Json.createObjectBuilder(); - addNonNull(rights, Prop.Rdfs.LABEL, uiAssetCreateRequest.getConditionsForUse()); - distribution.add(Prop.Dcterms.RIGHTS, rights); - } - - if (!hasDataModelFields && !hasReferenceFilesFields) { - return distribution; - } - var mobilityDataStandard = Json.createObjectBuilder(); - addNotBlank(mobilityDataStandard, Prop.ID, uiAssetCreateRequest.getDataModel()); - - if (hasReferenceFilesFields) { - var referenceFiles = Json.createObjectBuilder(); - addNonNullArray(referenceFiles, Prop.Dcat.DOWNLOAD_URL, uiAssetCreateRequest.getReferenceFileUrls()); - addNonNull(referenceFiles, Prop.Rdfs.LITERAL, uiAssetCreateRequest.getReferenceFilesDescription()); - mobilityDataStandard.add(Prop.MobilityDcatAp.SCHEMA, referenceFiles); - } - distribution.add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, mobilityDataStandard); - return distribution; - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java deleted file mode 100644 index ff5a5b515..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java +++ /dev/null @@ -1,263 +0,0 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers; - -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; -import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import lombok.SneakyThrows; -import net.javacrumbs.jsonunit.assertj.JsonAssertions; -import org.eclipse.edc.jsonld.TitaniumJsonLd; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.List; - -import static jakarta.json.Json.createArrayBuilder; -import static jakarta.json.Json.createObjectBuilder; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -class AssetMapperTest { - AssetMapper assetMapper; - - String endpoint = "https://my-connector/api/dsp"; - String participantId = "my-connector"; - - @BeforeEach - void setup() { - var jsonLd = new TitaniumJsonLd(mock(Monitor.class)); - var typeTransformerRegistry = mock(TypeTransformerRegistry.class); - var uiAssetBuilder = new UiAssetMapper(new EdcPropertyUtils(), new AssetJsonLdUtils(), new MarkdownToTextConverter(), new TextUtils(), x -> endpoint.equals(x)); - assetMapper = new AssetMapper(typeTransformerRegistry, uiAssetBuilder, jsonLd); - } - - @Test - @SneakyThrows - void test_buildAssetDto() { - // Arrange - String assetJsonLd = TestUtils.loadResourceAsString("/example-asset.jsonld"); - - // Act - var uiAsset = assetMapper.buildUiAsset(JsonUtils.parseJsonObj(assetJsonLd), endpoint, participantId); - - // Assert - assertThat(uiAsset.getAssetId()).isEqualTo("urn:artifact:my-asset"); - assertThat(uiAsset.getConnectorEndpoint()).isEqualTo(endpoint); - assertThat(uiAsset.getParticipantId()).isEqualTo(participantId); - assertThat(uiAsset.getTitle()).isEqualTo("My Asset"); - assertThat(uiAsset.getLanguage()).isEqualTo("https://w3id.org/idsa/code/EN"); - assertThat(uiAsset.getDescription()).isEqualTo( - "# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"); - assertThat(uiAsset.getDescriptionShortText()).isEqualTo( - "Lorem Ipsum... h2 title Link text Here 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); - assertThat(uiAsset.getIsOwnConnector()).isEqualTo(true); - assertThat(uiAsset.getCreatorOrganizationName()).isEqualTo("My Organization Name"); - assertThat(uiAsset.getPublisherHomepage()).isEqualTo("https://data-source.my-org/about"); - assertThat(uiAsset.getLicenseUrl()).isEqualTo("https://data-source.my-org/license"); - assertThat(uiAsset.getVersion()).isEqualTo("1.1"); - assertThat(uiAsset.getKeywords()).isEqualTo(List.of("some", "keywords")); - assertThat(uiAsset.getMediaType()).isEqualTo("application/json"); - assertThat(uiAsset.getLandingPageUrl()).isEqualTo("https://data-source.my-org/docs"); - assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isTrue(); - assertThat(uiAsset.getHttpDatasourceHintsProxyPath()).isTrue(); - assertThat(uiAsset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); - assertThat(uiAsset.getHttpDatasourceHintsProxyBody()).isTrue(); - assertThat(uiAsset.getDataCategory()).isEqualTo("Infrastructure and Logistics"); - assertThat(uiAsset.getDataSubcategory()).isEqualTo("General Information About Planning Of Routes"); - assertThat(uiAsset.getDataModel()).isEqualTo("my-data-model-001"); - assertThat(uiAsset.getGeoReferenceMethod()).isEqualTo("my-geo-reference-method"); - assertThat(uiAsset.getTransportMode()).isEqualTo("my-transport-mode"); - assertThat(uiAsset.getSovereignLegalName()).isEqualTo("my-sovereign"); - assertThat(uiAsset.getGeoLocation()).isEqualTo("my-geolocation"); - assertThat(uiAsset.getNutsLocations()).isEqualTo(Arrays.asList("my-nuts-location1", "my-nuts-location2")); - assertThat(uiAsset.getDataSampleUrls()).isEqualTo(Arrays.asList("my-data-sample-urls1", "my-data-sample-urls2")); - assertThat(uiAsset.getReferenceFileUrls()).isEqualTo(Arrays.asList("my-reference-files1", "my-reference-files2")); - assertThat(uiAsset.getReferenceFilesDescription()).isEqualTo("my-additional-description"); - assertThat(uiAsset.getConditionsForUse()).isEqualTo("my-conditions-for-use"); - assertThat(uiAsset.getDataUpdateFrequency()).isEqualTo("my-data-update-frequency"); - assertThat(uiAsset.getTemporalCoverageFrom()).isEqualTo("2007-12-03"); - assertThat(uiAsset.getTemporalCoverageToInclusive()).isEqualTo("2024-01-22"); - - assertThat(uiAsset.getAssetJsonLd()).contains("\"%s\"".formatted(Prop.Edc.ID)); - - JsonAssertions.assertThatJson(uiAsset.getCustomJsonAsString()) - .isEqualTo(""" - { - "array": [3, 1, 4, 1, 5], - "boolean": false, - "null": null, - "number": 116, - "object": { - "key": "value" - }, - "string": "value" - } - """); - JsonAssertions.assertThatJson(uiAsset.getCustomJsonLdAsString()) - .isObject() - .containsEntry("http://unknown/some-custom-string", "some-string-value") - .containsEntry("http://unknown/some-custom-obj", json(""" - { "http://unknown/a": "b" } - """)); - - JsonAssertions.assertThatJson(uiAsset.getPrivateCustomJsonAsString()) - .isEqualTo(""" - { - "priv_array": [3, 1, 4, 1, 5], - "priv_boolean": false, - "priv_null": null, - "priv_number": 116, - "priv_object": { - "key": "value" - }, - "priv_string": "value" - } - """); - JsonAssertions.assertThatJson(uiAsset.getPrivateCustomJsonLdAsString()) - .isObject() - .containsEntry("http://unknown/some-custom-private-string", "some-private-value") - .containsEntry("http://unknown/some-custom-private-obj", json(""" - { - "http://unknown/a-private": "b-private" - } - """)); - } - - @Test - void test_empty() { - - // Arrange - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .build(); - - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getAssetId()).isEqualTo("my-asset-1"); - assertThat(uiAsset.getTitle()).isEqualTo("my-asset-1"); - } - - @Test - void test_KeywordsAsSingleString() { - - // Arrange - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .add(Prop.Edc.PROPERTIES, createObjectBuilder() - .add(Prop.Dcat.KEYWORDS, "SingleElement") - .build()) - .build(); - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getKeywords()).isEqualTo(List.of("SingleElement")); - } - - @Test - void test_StringValueWrappedInAtValue() { - - // Arrange - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .add(Prop.Edc.PROPERTIES, createObjectBuilder() - .add(Prop.Dcterms.TITLE, createObjectBuilder() - .add(Prop.VALUE, "AssetTitle") - .add(Prop.LANGUAGE, "en"))) - .build(); - - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getTitle()).isEqualTo("AssetTitle"); - } - - @Test - void test_StringsAsMap() { - - // Arrange - var properties = createObjectBuilder() - .add(Prop.Dcterms.TITLE, createArrayBuilder() - .add(createObjectBuilder() - .add(Prop.TYPE, "SomeType") - .add(Prop.VALUE, "AssetTitle") - ) - ) - .build(); - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .add(Prop.Edc.PROPERTIES, properties) - .build(); - - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getTitle()).isEqualTo("AssetTitle"); - } - - @Test - void test_badBooleanValue() { - // Arrange - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .add(Prop.Edc.PROPERTIES, createObjectBuilder() - .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "wrongBooleanValue") - .build()) - .build(); - - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); - } - - @Test - void test_noBooleanValue() { - // Arrange - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .add(Prop.Edc.PROPERTIES, createObjectBuilder() - .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "") - .build()) - .build(); - - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); - } - - @Test - void test_isNotOwnConnector() { - // Arrange - var assetJsonLd = createObjectBuilder() - .add(Prop.ID, "my-asset-1") - .build(); - - // Act - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, "https://other-connector/api/dsp", participantId); - - // Assert - assertThat(uiAsset).isNotNull(); - assertThat(uiAsset.getIsOwnConnector()).isFalse(); - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java new file mode 100644 index 000000000..4c9d7093b --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.OwnConnectorEndpointService; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; + +import static org.mockito.Mockito.mock; + +public class Factory { + @NotNull + public static AssetMapper newAssetMapper( + TypeTransformerRegistry transformerRegistry, + OwnConnectorEndpointService ownConnectorEndpointService + ) { + return new AssetMapper( + transformerRegistry, + newAssetJsonLdBuilder(ownConnectorEndpointService), + newAssetJsonLdParser(ownConnectorEndpointService), + new TitaniumJsonLd(mock(Monitor.class)) + ); + } + + @NotNull + public static AssetJsonLdBuilder newAssetJsonLdBuilder(OwnConnectorEndpointService ownConnectorEndpointService) { + return new AssetJsonLdBuilder( + new DataSourceMapper( + new EdcPropertyUtils(), + new HttpDataSourceMapper(new HttpHeaderMapper()) + ), + newAssetJsonLdParser(ownConnectorEndpointService), + new AssetEditRequestMapper() + ); + } + + @NotNull + public static AssetJsonLdParser newAssetJsonLdParser(OwnConnectorEndpointService ownConnectorEndpointService) { + return new AssetJsonLdParser( + new AssetJsonLdUtils(), + new ShortDescriptionBuilder(), + ownConnectorEndpointService + ); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/JsonAssertsUtils.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/JsonAssertsUtils.java new file mode 100644 index 000000000..901b577d5 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/JsonAssertsUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.utils.JsonUtils; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import lombok.NonNull; +import org.apache.commons.collections4.CollectionUtils; +import org.jsoup.helper.Validate; + +import java.util.List; +import java.util.Map; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; + +public class JsonAssertsUtils { + + public static void assertEqualJson( + @NonNull JsonValue actual, + @NonNull JsonValue expected + ) { + + var actualJson = JsonUtils.toJson(actual); + var expectedJson = JsonUtils.toJson(expected); + try { + assertThatJson(actualJson).isEqualTo(expectedJson); + } catch (AssertionError e) { + var expectedStable = JsonUtils.toJsonPretty(stabilize(expected)); + var actualStable = JsonUtils.toJsonPretty(stabilize(actual)); + System.out.println("Actual: " + actualStable); + System.out.println("Expected: " + expectedStable); + throw new org.opentest4j.AssertionFailedError( + "JSONs are not equal. Click by IntelliJ to see a diff", + expectedStable, + actualStable + ); + } + } + + public static void assertIsEqualExcludingPaths( + JsonObject actual, + JsonObject expected, + List> exclude + ) { + var actualModified = JsonUtils.parseJsonObj(JsonUtils.toJson(actual)); + var expectedModified = JsonUtils.parseJsonObj(JsonUtils.toJson(expected)); + for (var path : exclude) { + actualModified = removePath(actualModified, path); + expectedModified = removePath(expectedModified, path); + } + + assertEqualJson(actualModified, expectedModified); + } + + private static JsonObject removePath(JsonObject obj, List path) { + Validate.isTrue(CollectionUtils.isNotEmpty(path), "path"); + if (obj == null) { + return null; + } + + var key = path.get(0); + if (!obj.containsKey(key)) { + return obj; + } + + if (path.size() == 1) { + return Json.createObjectBuilder(obj).remove(key).build(); + } + + var value = obj.getJsonObject(key); + value = removePath(value, path.subList(1, path.size())); + + return Json.createObjectBuilder(obj) + .remove(key) + .add(key, value) + .build(); + } + + private static JsonValue stabilize(JsonValue value) { + switch (value.getValueType()) { + case ARRAY: + var sorted = value.asJsonArray().stream() + .map(JsonAssertsUtils::stabilize) + .toList(); + return Json.createArrayBuilder(sorted).build(); + case OBJECT: + JsonObjectBuilder builder = Json.createObjectBuilder(); + value.asJsonObject().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> builder.add(entry.getKey(), stabilize(entry.getValue()))); + return builder.build(); + default: + return value; + } + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java index 9c12919d8..2d01fe7e7 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java @@ -1,8 +1,22 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.wrapper.api.common.mappers; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.model.AtomicConstraintDto; import de.sovity.edc.ext.wrapper.api.common.model.Expression; import de.sovity.edc.ext.wrapper.api.common.model.ExpressionType; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java new file mode 100644 index 000000000..0b01c947f --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset; + +import de.sovity.edc.ext.wrapper.api.common.mappers.Factory; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpDataMethod; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceOnRequest; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static de.sovity.edc.ext.wrapper.api.common.mappers.JsonAssertsUtils.assertEqualJson; +import static org.mockito.Mockito.mock; + +class AssetJsonLdBuilderTest { + AssetJsonLdBuilder assetJsonLdBuilder; + + private static final String ASSET_ID = "asset-id"; + private static final String ORG_NAME = "org-name"; + + @BeforeEach + void setup() { + var ownConnectorEndpointService = mock(OwnConnectorEndpointService.class); + assetJsonLdBuilder = Factory.newAssetJsonLdBuilder(ownConnectorEndpointService); + } + + @Test + void test_create_minimal() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .build(); + + var expectedProperties = Json.createObjectBuilder(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_nuts_empty() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .nutsLocations(List.of()) + .build(); + + var expectedProperties = Json.createObjectBuilder(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_referenceFiles_empty() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .referenceFileUrls(List.of()) + .build(); + + var expectedProperties = Json.createObjectBuilder(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_dataSampleUrls_empty() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .dataSampleUrls(List.of()) + .build(); + + var expectedProperties = Json.createObjectBuilder(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + // The following functions test paths of buildDistribution + @Test + void test_create_distribution_withMediaType() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .mediaType("B") + .build(); + + var expectedProperties = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.Dcat.MEDIATYPE, "B")); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_distribution_withConditionsForUse() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .conditionsForUse("B") + .build(); + + var expectedProperties = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.Dcterms.RIGHTS, Json.createObjectBuilder() + .add(Prop.Rdfs.LABEL, "B"))); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_distribution_withDataModel() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .dataModel("B") + .build(); + + var expectedProperties = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.ID, "B"))); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_distribution_withReferenceFileDescription() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .referenceFilesDescription("B") + .build(); + + var expectedProperties = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() + .add(Prop.Rdfs.LITERAL, "B")))); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_distribution_nullDataModel() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .dataModel(null) + .build(); + + var expected = dummyAssetJsonLd(Json.createObjectBuilder()); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, expected); + } + + @Test + void test_create_distribution_blankDataModel() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .dataModel(" ") + .build(); + + var expected = dummyAssetJsonLd(Json.createObjectBuilder()); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, expected); + } + + @Test + void test_create_distribution_dataModelBlank_withReferenceFilesDesc() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .dataModel(" ") + .referenceFilesDescription("test") + .build(); + + var expectedProperties = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() + .add(Prop.Rdfs.LITERAL, "test")))); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_distribution_dataModelBlank_withReferenceFileUrls() { + // arrange + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dummyDataSource()) + .id(ASSET_ID) + .dataModel(" ") + .referenceFileUrls(List.of("http://test")) + .build(); + + var expectedProperties = Json.createObjectBuilder() + .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() + .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() + .add(Prop.Dcat.DOWNLOAD_URL, Json.createArrayBuilder().add("http://test"))))); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(expectedProperties)); + } + + @Test + void test_create_httpData_full() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .method(UiDataSourceHttpDataMethod.PUT) + .baseUrl("https://example.com") + .queryString("a=b") + .headers(Map.of("c", "d", "Content-Type", "e")) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.METHOD, "PUT") + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.QUERY_PARAMS, "a=b") + .add("header:c", "d") + .add(Prop.Edc.CONTENT_TYPE, "e"); + + var expectedProperties = dummyAssetCommonProperties(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + @Test + void test_create_httpData_methodParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enableMethodParameterization(true) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.PROXY_METHOD, "true"); + + var expectedProperties = dummyAssetCommonProperties() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "true"); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + @Test + void test_create_httpData_pathParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enablePathParameterization(true) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.PROXY_PATH, "true"); + + var expectedProperties = dummyAssetCommonProperties() + .add(Prop.SovityDcatExt.HttpDatasourceHints.PATH, "true"); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + @Test + void test_create_httpData_queryParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enableQueryParameterization(true) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.PROXY_QUERY_PARAMS, "true"); + + var expectedProperties = dummyAssetCommonProperties() + .add(Prop.SovityDcatExt.HttpDatasourceHints.QUERY_PARAMS, "true"); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + @Test + void test_create_httpData_bodyParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enableBodyParameterization(true) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.PROXY_BODY, "true"); + + var expectedProperties = dummyAssetCommonProperties() + .add(Prop.SovityDcatExt.HttpDatasourceHints.BODY, "true"); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + @Test + void test_create_onRequest() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("contact@example.com") + .contactPreferredEmailSubject("Test") + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "http://0.0.0.0") + .add(Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY, Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY_ON_REQUEST) + .add(Prop.SovityDcatExt.CONTACT_EMAIL, "contact@example.com") + .add(Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT, "Test"); + + var expectedProperties = dummyAssetCommonProperties() + .add(Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY, Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY_ON_REQUEST) + .add(Prop.SovityDcatExt.CONTACT_EMAIL, "contact@example.com") + .add(Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT, "Test") + .remove(Prop.SovityDcatExt.HttpDatasourceHints.METHOD) + .remove(Prop.SovityDcatExt.HttpDatasourceHints.PATH) + .remove(Prop.SovityDcatExt.HttpDatasourceHints.QUERY_PARAMS) + .remove(Prop.SovityDcatExt.HttpDatasourceHints.BODY); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + private JsonObject dummyAssetJsonLd( + JsonObjectBuilder dataAddress, + JsonObjectBuilder properties + ) { + return Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_ASSET) + .add(Prop.ID, ASSET_ID) + .add(Prop.Edc.DATA_ADDRESS, dataAddress) + .add(Prop.Edc.PROPERTIES, properties) + .add(Prop.Edc.PRIVATE_PROPERTIES, Json.createObjectBuilder()) + .build(); + } + + private JsonObject dummyAssetJsonLd( + JsonObjectBuilder properties + ) { + var dataAddress = dummyDataAddressJsonLd(); + properties = properties.addAll(dummyAssetCommonProperties()); + return dummyAssetJsonLd(dataAddress, properties); + } + + private JsonObjectBuilder dummyAssetCommonProperties() { + return Json.createObjectBuilder() + .add(Prop.Edc.ID, ASSET_ID) + .add(Prop.Dcterms.CREATOR, Json.createObjectBuilder() + .add(Prop.Foaf.NAME, ORG_NAME)) + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "false") + .add(Prop.SovityDcatExt.HttpDatasourceHints.PATH, "false") + .add(Prop.SovityDcatExt.HttpDatasourceHints.QUERY_PARAMS, "false") + .add(Prop.SovityDcatExt.HttpDatasourceHints.BODY, "false"); + } + + private UiDataSource dummyDataSource() { + return UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .build()) + .build(); + } + + private JsonObjectBuilder dummyDataAddressJsonLd() { + return Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com"); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParserTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParserTest.java new file mode 100644 index 000000000..b629d55af --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParserTest.java @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import de.sovity.edc.ext.wrapper.api.common.mappers.Factory; +import de.sovity.edc.ext.wrapper.api.common.mappers.TestUtils; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceAvailability; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; +import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceOnRequest; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static de.sovity.edc.ext.wrapper.api.common.mappers.JsonAssertsUtils.assertEqualJson; +import static de.sovity.edc.ext.wrapper.api.common.mappers.JsonAssertsUtils.assertIsEqualExcludingPaths; +import static jakarta.json.Json.createArrayBuilder; +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class AssetJsonLdParserTest { + AssetJsonLdBuilder assetJsonLdBuilder; + AssetJsonLdParser assetJsonLdParser; + ObjectMapper objectMapper; + + private static final String ASSET_ID = "asset-id"; + private static final String ORG_NAME = "org-name"; + private static final String ENDPOINT = "endpoint"; + private static final String PARTICIPANT_ID = "participant-id"; + + @BeforeEach + void setup() { + var ownConnectorEndpointService = mock(OwnConnectorEndpointService.class); + assetJsonLdBuilder = Factory.newAssetJsonLdBuilder(ownConnectorEndpointService); + assetJsonLdParser = Factory.newAssetJsonLdParser(ownConnectorEndpointService); + objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + @Test + @SneakyThrows + void test_buildUiAsset_full() { + // arrange + var assetJsonLdString = TestUtils.loadResourceAsString("/example-asset-json-ld.json"); + var assetJsonString = TestUtils.loadResourceAsString("/example-ui-asset.json"); + + var assetJsonLd = JsonUtils.parseJsonObj(assetJsonLdString); + var assetJson = JsonUtils.parseJsonObj(assetJsonString); + + // act + var actualUiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // assert + var uiAssetJson = JsonUtils.parseJsonObj(objectMapper.writeValueAsString(actualUiAsset)); + var actualAssetJsonLd = JsonUtils.parseJsonObj(uiAssetJson.getString("assetJsonLd")); + assertEqualJson( + actualAssetJsonLd, + assetJsonLd + ); + + assertIsEqualExcludingPaths( + uiAssetJson, + assetJson, + List.of(List.of("assetJsonLd")) + ); + } + + @Test + void test_empty() { + + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getAssetId()).isEqualTo("my-asset-1"); + assertThat(uiAsset.getTitle()).isEqualTo("my-asset-1"); + } + + @Test + void test_KeywordsAsSingleString() { + + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.Dcat.KEYWORDS, "SingleElement") + .build()) + .build(); + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getKeywords()).isEqualTo(List.of("SingleElement")); + } + + @Test + void test_StringValueWrappedInAtValue() { + + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.Dcterms.TITLE, createObjectBuilder() + .add(Prop.VALUE, "AssetTitle") + .add(Prop.LANGUAGE, "en"))) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getTitle()).isEqualTo("AssetTitle"); + } + + @Test + void test_jsonld_utils_deserializing_nested_value() { + + // Arrange + var properties = createObjectBuilder() + .add(Prop.Dcterms.TITLE, createArrayBuilder() + .add(createObjectBuilder() + .add(Prop.TYPE, "SomeType") + .add(Prop.VALUE, "AssetTitle") + ) + ) + .build(); + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, properties) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getTitle()).isEqualTo("AssetTitle"); + } + + @Test + void test_createUiAsset_booleanParsing_trueValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "true") + .build()) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isTrue(); + } + + @Test + void test_createUiAsset_booleanParsing_falseValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "false") + .build()) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isFalse(); + } + + @Test + void test_createUiAsset_booleanParsing_badValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, "wrongBooleanValue") + .build()) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); + } + + @Test + void test_createUiAsset_booleanParsing_blankBooleanValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .add(Prop.SovityDcatExt.HttpDatasourceHints.METHOD, " ") + .build()) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); + } + + @Test + void test_createUiAsset_booleanParsing_noBooleanValue() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .add(Prop.Edc.PROPERTIES, createObjectBuilder() + .build()) + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, ENDPOINT, PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getHttpDatasourceHintsProxyMethod()).isNull(); + } + + @Test + void test_isNotOwnConnector() { + // Arrange + var assetJsonLd = createObjectBuilder() + .add(Prop.ID, "my-asset-1") + .build(); + + // Act + var uiAsset = assetJsonLdParser.buildUiAsset(assetJsonLd, "https://other-connector/api/dsp", PARTICIPANT_ID); + + // Assert + assertThat(uiAsset).isNotNull(); + assertThat(uiAsset.getIsOwnConnector()).isFalse(); + } + + @Test + void test_buildUiAsset_httpData_methodParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enableMethodParameterization(true) + .build()) + .build(); + + var createRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var expected = dummyUiAsset() + .dataSourceAvailability(DataSourceAvailability.LIVE) + .httpDatasourceHintsProxyMethod(true) + .build(); + + + // act + var jsonLd = assetJsonLdBuilder.createAssetJsonLd(createRequest, ORG_NAME); + var actual = assetJsonLdParser.buildUiAsset(jsonLd, ENDPOINT, PARTICIPANT_ID); + + // assert + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("assetJsonLd") + .isEqualTo(expected); + } + + @Test + void test_buildUiAsset_httpData_pathParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enablePathParameterization(true) + .build()) + .build(); + + var createRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var expected = dummyUiAsset() + .dataSourceAvailability(DataSourceAvailability.LIVE) + .httpDatasourceHintsProxyPath(true) + .build(); + + // act + var jsonLd = assetJsonLdBuilder.createAssetJsonLd(createRequest, ORG_NAME); + var actual = assetJsonLdParser.buildUiAsset(jsonLd, ENDPOINT, PARTICIPANT_ID); + + // assert + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("assetJsonLd") + .isEqualTo(expected); + } + + @Test + void test_buildUiAsset_httpData_queryParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enableQueryParameterization(true) + .build()) + .build(); + + var createRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var expected = dummyUiAsset() + .dataSourceAvailability(DataSourceAvailability.LIVE) + .httpDatasourceHintsProxyQueryParams(true) + .build(); + + // act + var jsonLd = assetJsonLdBuilder.createAssetJsonLd(createRequest, ORG_NAME); + var actual = assetJsonLdParser.buildUiAsset(jsonLd, ENDPOINT, PARTICIPANT_ID); + + // assert + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("assetJsonLd") + .isEqualTo(expected); + } + + @Test + void test_buildUiAsset_httpData_bodyParameterization() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .enableBodyParameterization(true) + .build()) + .build(); + + var createRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var expected = dummyUiAsset() + .dataSourceAvailability(DataSourceAvailability.LIVE) + .httpDatasourceHintsProxyBody(true) + .build(); + + // act + var jsonLd = assetJsonLdBuilder.createAssetJsonLd(createRequest, ORG_NAME); + var actual = assetJsonLdParser.buildUiAsset(jsonLd, ENDPOINT, PARTICIPANT_ID); + + // assert + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("assetJsonLd") + .isEqualTo(expected); + } + + @Test + void test_buildUiAsset_onRequest() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("contact@example.com") + .contactPreferredEmailSubject("Test") + .build()) + .build(); + + var createRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var expected = dummyUiAsset() + .assetId(ASSET_ID) + .dataSourceAvailability(DataSourceAvailability.ON_REQUEST) + .onRequestContactEmail("contact@example.com") + .onRequestContactEmailSubject("Test") + .httpDatasourceHintsProxyMethod(null) + .httpDatasourceHintsProxyPath(null) + .httpDatasourceHintsProxyQueryParams(null) + .httpDatasourceHintsProxyBody(null) + .build(); + + // act + var jsonLd = assetJsonLdBuilder.createAssetJsonLd(createRequest, ORG_NAME); + var actual = assetJsonLdParser.buildUiAsset(jsonLd, ENDPOINT, PARTICIPANT_ID); + + // assert + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("assetJsonLd") + .isEqualTo(expected); + } + + private UiAsset.UiAssetBuilder dummyUiAsset() { + return UiAsset.builder() + .connectorEndpoint(ENDPOINT) + .isOwnConnector(false) + .creatorOrganizationName(ORG_NAME) + .participantId(PARTICIPANT_ID) + .assetId(ASSET_ID) + .title(ASSET_ID) + .dataSampleUrls(List.of()) + .keywords(List.of()) + .nutsLocations(List.of()) + .referenceFileUrls(List.of()) + .customJsonLdAsString("{}") + .privateCustomJsonLdAsString("{}") + .httpDatasourceHintsProxyMethod(false) + .httpDatasourceHintsProxyPath(false) + .httpDatasourceHintsProxyQueryParams(false) + .httpDatasourceHintsProxyBody(false); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java similarity index 91% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java index d95246d45..87f593789 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/EdcPropertyUtilsTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java @@ -12,8 +12,9 @@ * */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java new file mode 100644 index 000000000..480b584f1 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class ShortDescriptionBuilderTest { + @InjectMocks + ShortDescriptionBuilder textUtils; + + @Test + void test_shortDescription_null() { + // arrange + String text = null; + + // act + var actual = textUtils.buildShortDescription(text); + + // assert + assertThat(actual).isNull(); + } + + @Test + void test_shortDescription_exceedsLength() { + // arrange + var text = + "# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; + var expected = + "Lorem Ipsum... h2 title Link text Here 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + + // act + var actual = textUtils.buildShortDescription(text); + + // assert + assertThat(actual).isEqualTo(expected); + } + + @Test + void test_abbreviate_null() { + // arrange + String text = null; + + // act + var actual = textUtils.abbreviate(text, 1); + + // assert + assertThat(actual).isNull(); + } + + @Test + void test_abbreviate_emptyString() { + // arrange + var text = ""; + + // act + var actual = textUtils.abbreviate(text, 1); + + // assert + assertThat(actual).isEqualTo(""); + } + + @Test + void test_abbreviate_lengthLessThanMaxCharacters() { + // arrange + var text = "a"; + + // act + var actual = textUtils.abbreviate(text, 2); + + // assert + assertThat(actual).isEqualTo("a"); + } + + @Test + void test_abbreviate_lengthLongerThanMaxCharacters() { + // arrange + var text = "aa"; + + // act + var actual = textUtils.abbreviate(text, 1); + + // assert + assertThat(actual).isEqualTo("a"); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java similarity index 95% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java index 70fed07d0..0a3b718f4 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/AtomicConstraintMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java @@ -1,7 +1,20 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java similarity index 82% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java index 9b9d52c55..4f7730469 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ConstraintExtractorTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java @@ -1,5 +1,23 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import org.eclipse.edc.policy.model.AndConstraint; import org.eclipse.edc.policy.model.AtomicConstraint; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java similarity index 92% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java index 2f292c888..3c4786117 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/LiteralMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java @@ -1,6 +1,22 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteralType; import org.eclipse.edc.policy.model.Expression; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java similarity index 53% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java index 6512cbf15..a217ffa88 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/MappingErrorsTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java @@ -1,5 +1,20 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java similarity index 74% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java index 70af31f2f..18635313f 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/OperatorMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java @@ -1,5 +1,20 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers; +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import org.assertj.core.api.Assertions; import org.eclipse.edc.policy.model.Operator; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java similarity index 85% rename from extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java rename to extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java index e18a79bd4..1e28f8ebf 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/PolicyValidatorTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java @@ -1,5 +1,35 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.Constraint; import org.eclipse.edc.policy.model.Duty; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java deleted file mode 100644 index 92ffe3c80..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/TextUtilsTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class TextUtilsTest { - TextUtils textUtils; - - @BeforeEach - void setup() { - textUtils = new TextUtils(); - } - - @Test - void test_abbreviate_null() { - // arrange - String text = null; - - // act - var actual = textUtils.abbreviate(text, 1); - - // assert - assertThat(actual).isEqualTo(null); - } - - @Test - void test_abbreviate_emptyString() { - // arrange - var text = ""; - - // act - var actual = textUtils.abbreviate(text, 1); - - // assert - assertThat(actual).isEqualTo(""); - } - - @Test - void test_abbreviate_lengthLessThanMaxCharacters() { - // arrange - var text = "a"; - - // act - var actual = textUtils.abbreviate(text, 2); - - // assert - assertThat(actual).isEqualTo("a"); - } - - @Test - void test_abbreviate_lengthLongerThanMaxCharacters() { - // arrange - var text = "aa"; - - // act - var actual = textUtils.abbreviate(text, 1); - - // assert - assertThat(actual).isEqualTo("a"); - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java deleted file mode 100644 index 27b21a0d9..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapperTest.java +++ /dev/null @@ -1,306 +0,0 @@ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; - -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -class UiAssetMapperTest { - UiAssetMapper uiAssetMapper; - EdcPropertyUtils edcPropertyUtils; - - private static final String ASSET_ID = "asset-id"; - private static final String ORG_NAME = "org-name"; - - @BeforeEach - void setup() { - var assetJsonLdUtils = mock(AssetJsonLdUtils.class); - var markdownToTextConverter = mock(MarkdownToTextConverter.class); - var textUtils = mock(TextUtils.class); - var ownConnectorEndpointService = mock(OwnConnectorEndpointService.class); - edcPropertyUtils = new EdcPropertyUtils(); - - uiAssetMapper = new UiAssetMapper(edcPropertyUtils, assetJsonLdUtils, markdownToTextConverter, textUtils, ownConnectorEndpointService); - } - - @Test - void test_buildAssetJsonLd_only_id() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); - assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); - } - - @Test - void test_buildAssetJsonLd_empty_nuts() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setNutsLocations(List.of()); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); - } - - @Test - void test_buildAssetJsonLd_empty_reference_file_urls() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setReferenceFileUrls(List.of()); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); - } - - @Test - void test_buildAssetJsonLd_empty_data_sample_urls() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataSampleUrls(List.of()); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getJsonObject(Prop.Edc.PROPERTIES)).hasSize(2); - } - - // The following functions test paths of buildDistribution - @Test - void test_buildAssetJsonLd_distribution1() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setMediaType("B"); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); - var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); - assertThat(properties).hasSize(3); - var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); - assertThat(distribution).hasSize(1); - assertThat(distribution.getString(Prop.Dcat.MEDIATYPE)).isEqualTo("B"); - } - - @Test - void test_buildAssetJsonLd_distribution2() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setConditionsForUse("B"); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); - var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); - assertThat(properties).hasSize(3); - var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); - assertThat(distribution).hasSize(1); - var rights = distribution.getJsonObject(Prop.Dcterms.RIGHTS); - assertThat(rights).hasSize(1); - assertThat(rights.getString(Prop.Rdfs.LABEL)).isEqualTo("B"); - } - - @Test - void test_buildAssetJsonLd_distribution3() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataModel("B"); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); - var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); - assertThat(properties).hasSize(3); - var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); - assertThat(distribution).hasSize(1); - var mobilityDataStandard = distribution.getJsonObject(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); - assertThat(mobilityDataStandard).hasSize(1); - assertThat(mobilityDataStandard.getString(Prop.ID)).isEqualTo("B"); - } - - @Test - void test_buildAssetJsonLd_distribution4() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setReferenceFilesDescription("B"); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThat(actual).isNotNull(); - assertThat(actual.getString(Prop.ID)).isEqualTo(ASSET_ID); - var properties = actual.getJsonObject(Prop.Edc.PROPERTIES); - assertThat(properties).hasSize(3); - var distribution = properties.getJsonObject(Prop.Dcat.DISTRIBUTION); - assertThat(distribution).hasSize(1); - var mobilityDataStandard = distribution.getJsonObject(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD); - assertThat(mobilityDataStandard).hasSize(1); - var referenceFiles = mobilityDataStandard.getJsonObject(Prop.MobilityDcatAp.SCHEMA); - assertThat(referenceFiles).hasSize(1); - assertThat(referenceFiles.getString(Prop.Rdfs.LITERAL)).isEqualTo("B"); - } - - @Test - void test_buildAssetJsonLd_data_model_nonNull() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataModel("B"); - - var expected = Json.createObjectBuilder() - .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() - .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() - .add(Prop.ID, "B"))); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThatJson(JsonUtils.toJson(actual)) - .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); - } - - @Test - void test_buildAssetJsonLd_data_model_null() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataModel(null); - - var expected = Json.createObjectBuilder(); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThatJson(JsonUtils.toJson(actual)) - .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); - } - - @Test - void test_buildAssetJsonLd_data_model_blank() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataModel(" "); - - var expected = Json.createObjectBuilder(); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThatJson(JsonUtils.toJson(actual)) - .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); - } - - @Test - void test_buildAssetJsonLd_data_model_blank_but_also_reference_files_desc() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataModel(" "); - uiAssetCreateRequest.setReferenceFilesDescription("test"); - - var expected = Json.createObjectBuilder() - .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() - .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() - .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() - .add(Prop.Rdfs.LITERAL, "test")))); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThatJson(JsonUtils.toJson(actual)) - .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); - } - - @Test - void test_buildAssetJsonLd_data_model_blank_but_also_reference_file_urls() { - // arrange - var uiAssetCreateRequest = new UiAssetCreateRequest(); - uiAssetCreateRequest.setId(ASSET_ID); - uiAssetCreateRequest.setDataModel(" "); - uiAssetCreateRequest.setReferenceFileUrls(List.of("http://test")); - - var expected = Json.createObjectBuilder() - .add(Prop.Dcat.DISTRIBUTION, Json.createObjectBuilder() - .add(Prop.MobilityDcatAp.MOBILITY_DATA_STANDARD, Json.createObjectBuilder() - .add(Prop.MobilityDcatAp.SCHEMA, Json.createObjectBuilder() - .add(Prop.Dcat.DOWNLOAD_URL, Json.createArrayBuilder().add("http://test"))))); - - // act - var actual = uiAssetMapper.buildAssetJsonLd(uiAssetCreateRequest, ORG_NAME); - - // assert - assertThatJson(JsonUtils.toJson(actual)) - .isEqualTo(JsonUtils.toJson(buildAssetJsonLd(expected))); - } - - /** - * Creates Asset JSON LD from additional properties - *

      - * Let the above tests be more readable - * - * @param properties additional Asset JSON-LD Properties - * @return Asset JSON LD - */ - private JsonObject buildAssetJsonLd(JsonObjectBuilder properties) { - return Json.createObjectBuilder() - .add(Prop.TYPE, Prop.Edc.TYPE_ASSET) - .add(Prop.ID, ASSET_ID) - .add(Prop.Edc.DATA_ADDRESS, Json.createObjectBuilder() - .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) - .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder())) - .add(Prop.Edc.PROPERTIES, properties - .add(Prop.Edc.ID, ASSET_ID) - .add(Prop.Dcterms.CREATOR, Json.createObjectBuilder() - .add(Prop.Foaf.NAME, ORG_NAME)) - ) - .add(Prop.Edc.PRIVATE_PROPERTIES, Json.createObjectBuilder()) - .build(); - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset-json-ld.json similarity index 93% rename from extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld rename to extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset-json-ld.json index 1334b829c..f63271a20 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld +++ b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset-json-ld.json @@ -43,10 +43,6 @@ "keywords" ], "http://www.w3.org/ns/dcat#landingPage": "https://data-source.my-org/docs", - "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod": "true", - "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath": "true", - "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams": "true", - "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody": "true", "https://w3id.org/mobilitydcat-ap/mobilityTheme": { "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-category": "Infrastructure and Logistics", "https://w3id.org/mobilitydcat-ap/mobility-theme/data-content-sub-category": "General Information About Planning Of Routes" @@ -68,7 +64,11 @@ }, "https://semantic.sovity.io/dcat-ext#customJson": "{\"array\":[3,1,4,1,5],\"boolean\":false,\"null\":null,\"number\":116,\"object\":{\"key\":\"value\"},\"string\":\"value\"}", "http://unknown/some-custom-string": "some-string-value", - "http://unknown/some-custom-obj": {"http://unknown/a": "b"} + "http://unknown/some-custom-obj": {"http://unknown/a": "b"}, + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyMethod": "true", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyPath": "true", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyQueryParams": "true", + "https://semantic.sovity.io/dcat-ext#httpDatasourceHintsProxyBody": "true" }, "https://w3id.org/edc/v0.0.1/ns/privateProperties": { "https://semantic.sovity.io/dcat-ext#privateCustomJson":"{\"priv_array\":[3,1,4,1,5],\"priv_boolean\":false,\"priv_null\":null,\"priv_number\":116,\"priv_object\":{\"key\":\"value\"},\"priv_string\":\"value\"}", @@ -77,6 +77,10 @@ }, "https://w3id.org/edc/v0.0.1/ns/DataAddress": { "https://w3id.org/edc/v0.0.1/ns/type": "HttpData", - "https://w3id.org/edc/v0.0.1/ns/baseUrl": "https://data-source.my-org" + "https://w3id.org/edc/v0.0.1/ns/baseUrl": "https://data-source.my-org", + "https://w3id.org/edc/v0.0.1/ns/proxyMethod": "true", + "https://w3id.org/edc/v0.0.1/ns/proxyPath": "true", + "https://w3id.org/edc/v0.0.1/ns/proxyQueryParams": "true", + "https://w3id.org/edc/v0.0.1/ns/proxyBody": "true" } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-ui-asset.json b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-ui-asset.json new file mode 100644 index 000000000..fa6820dec --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-ui-asset.json @@ -0,0 +1,53 @@ +{ + "isOwnConnector": false, + "connectorEndpoint": "endpoint", + "participantId": "participant-id", + "assetId": "urn:artifact:my-asset", + "title": "My Asset", + "language": "https://w3id.org/idsa/code/EN", + "description": "# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "descriptionShortText": "Lorem Ipsum... h2 title Link text Here 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", + "creatorOrganizationName": "My Organization Name", + "mediaType": "application/json", + "dataModel": "my-data-model-001", + "referenceFileUrls": [ + "my-reference-files1", + "my-reference-files2" + ], + "referenceFilesDescription": "my-additional-description", + "dataSampleUrls": [ + "my-data-sample-urls1", + "my-data-sample-urls2" + ], + "conditionsForUse": "my-conditions-for-use", + "publisherHomepage": "https://data-source.my-org/about", + "licenseUrl": "https://data-source.my-org/license", + "version": "1.1", + "keywords": [ + "some", + "keywords" + ], + "landingPageUrl": "https://data-source.my-org/docs", + "dataCategory": "Infrastructure and Logistics", + "dataSubcategory": "General Information About Planning Of Routes", + "geoReferenceMethod": "my-geo-reference-method", + "transportMode": "my-transport-mode", + "sovereignLegalName": "my-sovereign", + "geoLocation": "my-geolocation", + "nutsLocations": [ + "my-nuts-location1", + "my-nuts-location2" + ], + "dataUpdateFrequency": "my-data-update-frequency", + "temporalCoverageFrom": "2007-12-03", + "temporalCoverageToInclusive": "2024-01-22", + "customJsonAsString": "{\"array\":[3,1,4,1,5],\"boolean\":false,\"null\":null,\"number\":116,\"object\":{\"key\":\"value\"},\"string\":\"value\"}", + "customJsonLdAsString": "{\"http://unknown/some-custom-string\":\"some-string-value\",\"http://unknown/some-custom-obj\":{\"http://unknown/a\":\"b\"}}", + "privateCustomJsonAsString": "{\"priv_array\":[3,1,4,1,5],\"priv_boolean\":false,\"priv_null\":null,\"priv_number\":116,\"priv_object\":{\"key\":\"value\"},\"priv_string\":\"value\"}", + "privateCustomJsonLdAsString": "{\"http://unknown/some-custom-private-string\":\"some-private-value\",\"http://unknown/some-custom-private-obj\":{\"http://unknown/a-private\":\"b-private\"}}", + "dataSourceAvailability": "LIVE", + "httpDatasourceHintsProxyMethod": true, + "httpDatasourceHintsProxyPath": true, + "httpDatasourceHintsProxyQueryParams": true, + "httpDatasourceHintsProxyBody": true +} diff --git a/extensions/wrapper/wrapper-ee-api/README.md b/extensions/wrapper/wrapper-ee-api/README.md index 0902aa563..2612458b5 100644 --- a/extensions/wrapper/wrapper-ee-api/README.md +++ b/extensions/wrapper/wrapper-ee-api/README.md @@ -1,7 +1,7 @@

      - + Logo @@ -9,9 +9,9 @@ Specification

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/wrapper/README.md b/extensions/wrapper/wrapper/README.md index 48b5bb007..6197a9628 100644 --- a/extensions/wrapper/wrapper/README.md +++ b/extensions/wrapper/wrapper/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Extension:
      API Wrapper & API Clients:
      Community Edition API Wrapper Implementation

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts index 840ce4349..55b88a8f2 100644 --- a/extensions/wrapper/wrapper/build.gradle.kts +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -57,10 +57,6 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -tasks.withType { - maxParallelForks = 1 -} - group = libs.versions.sovityEdcExtensionGroup.get() publishing { diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 8f2aa2ae8..36074d5ed 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -16,21 +16,24 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AssetJsonLdUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.LiteralMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.MarkdownToTextConverter; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.PolicyValidator; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.TextUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.UiAssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.OwnConnectorEndpointService; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import de.sovity.edc.ext.wrapper.api.ui.UiResourceImpl; import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetApiService; -import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder; import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetIdValidator; import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.CatalogApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.UiDataOfferBuilder; @@ -40,6 +43,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementPageCardBuilder; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementUtils; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractNegotiationUtils; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ParameterizationCompatibilityUtils; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.TransferRequestBuilder; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionBuilder; @@ -88,6 +92,7 @@ import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; import java.util.List; @@ -137,19 +142,9 @@ public static WrapperExtensionContext buildContext( atomicConstraintMapper, typeTransformerRegistry); var edcPropertyUtils = new EdcPropertyUtils(); - var assetJsonLdUtils = new AssetJsonLdUtils(); - var markdownToTextConverter = new MarkdownToTextConverter(); - var textUtils = new TextUtils(); var selfDescriptionService = new SelfDescriptionService(config, monitor); var ownConnectorEndpointService = new OwnConnectorEndpointServiceImpl(selfDescriptionService); - var uiAssetMapper = new UiAssetMapper( - edcPropertyUtils, - assetJsonLdUtils, - markdownToTextConverter, - textUtils, - ownConnectorEndpointService - ); - var assetMapper = new AssetMapper(typeTransformerRegistry, uiAssetMapper, jsonLd); + var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd, ownConnectorEndpointService); var transferProcessStateService = new TransferProcessStateService(); var contractNegotiationUtils = new ContractNegotiationUtils( contractNegotiationService, @@ -193,16 +188,10 @@ public static WrapperExtensionContext buildContext( var contractAgreementUtils = new ContractAgreementUtils(contractAgreementService); var parameterizationCompatibilityUtils = new ParameterizationCompatibilityUtils(); var assetIdValidator = new AssetIdValidator(); - var assetBuilder = new AssetBuilder( - assetMapper, - edcPropertyUtils, - assetIdValidator, - selfDescriptionService - ); var assetApiService = new AssetApiService( assetService, assetMapper, - assetBuilder, + assetIdValidator, selfDescriptionService ); var transferRequestBuilder = new TransferRequestBuilder( @@ -299,4 +288,38 @@ public static WrapperExtensionContext buildContext( useCaseResource ), selfDescriptionService); } + + @NotNull + private static AssetMapper newAssetMapper( + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd, + OwnConnectorEndpointService ownConnectorEndpointService + ) { + var edcPropertyUtils = new EdcPropertyUtils(); + var assetJsonLdUtils = new AssetJsonLdUtils(); + var assetEditRequestMapper = new AssetEditRequestMapper(); + var shortDescriptionBuilder = new ShortDescriptionBuilder(); + var assetJsonLdParser = new AssetJsonLdParser( + assetJsonLdUtils, + shortDescriptionBuilder, + ownConnectorEndpointService + ); + var httpHeaderMapper = new HttpHeaderMapper(); + var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); + var dataSourceMapper = new DataSourceMapper( + edcPropertyUtils, + httpDataSourceMapper + ); + var assetJsonLdBuilder = new AssetJsonLdBuilder( + dataSourceMapper, + assetJsonLdParser, + assetEditRequestMapper + ); + return new AssetMapper( + typeTransformerRegistry, + assetJsonLdBuilder, + assetJsonLdParser, + jsonLd + ); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java deleted file mode 100644 index 115ab8643..000000000 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ApiInformation.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.wrapper.api; - -import io.swagger.v3.oas.annotations.ExternalDocumentation; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.info.License; - -@OpenAPIDefinition( - info = @Info( - title = "sovity EDC API Wrapper", - version = "0.0.0", - description = "sovity's EDC API Wrapper contains a selection of APIs for multiple consumers, " + - "e.g. our EDC UI API, our generic Use Case API, our Commercial APIs, etc. " + - "We bundled these APIs, so we can have an easier time generating our API Client Libraries.", - contact = @Contact( - name = "Sovity GmbH", - email = "contact@sovity.de", - url = "https://github.com/sovity/edc-extensions/issues/new/choose" - ), - license = @License( - name = "Apache 2.0", - url = "https://github.com/sovity/edc-extensions/blob/main/LICENSE" - ) - ), - externalDocs = @ExternalDocumentation( - description = "EDC API Wrapper Project in sovity/edc-extensions", - url = "https://github.com/sovity/edc-extensions/tree/main/extensions/wrapper" - ) -) -public interface ApiInformation { -} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 9e9aed244..4aba52060 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -17,7 +17,7 @@ import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; @@ -76,8 +76,8 @@ public IdResponseDto createAsset(UiAssetCreateRequest uiAssetCreateRequest) { } @Override - public IdResponseDto editAssetMetadata(String assetId, UiAssetEditMetadataRequest uiAssetEditMetadataRequest) { - return assetApiService.editAsset(assetId, uiAssetEditMetadataRequest); + public IdResponseDto editAssetMetadata(String assetId, UiAssetEditRequest uiAssetEditRequest) { + return assetApiService.editAsset(assetId, uiAssetEditRequest); } @Override diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java index 3e3206015..d4ba7ea02 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java @@ -18,7 +18,7 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; import lombok.RequiredArgsConstructor; @@ -36,7 +36,7 @@ public class AssetApiService { private final AssetService assetService; private final AssetMapper assetMapper; - private final AssetBuilder assetBuilder; + private final AssetIdValidator assetIdValidator; private final SelfDescriptionService selfDescriptionService; public List getAssets() { @@ -44,21 +44,23 @@ public List getAssets() { var connectorEndpoint = selfDescriptionService.getConnectorEndpoint(); var participantId = selfDescriptionService.getParticipantId(); return assets.stream().sorted(Comparator.comparing(Asset::getCreatedAt).reversed()) - .map(asset -> assetMapper.buildUiAsset(asset, connectorEndpoint, participantId)) - .toList(); + .map(asset -> assetMapper.buildUiAsset(asset, connectorEndpoint, participantId)) + .toList(); } @NotNull public IdResponseDto createAsset(UiAssetCreateRequest request) { - val asset = assetBuilder.fromCreateRequest(request); + assetIdValidator.assertValid(request.getId()); + var organizationName = selfDescriptionService.getCuratorName(); + val asset = assetMapper.buildAsset(request, organizationName); val createdAsset = assetService.create(asset).orElseThrow(ServiceException::new); return new IdResponseDto(createdAsset.getId()); } - public IdResponseDto editAsset(String assetId, UiAssetEditMetadataRequest request) { + public IdResponseDto editAsset(String assetId, UiAssetEditRequest request) { val foundAsset = assetService.findById(assetId); Objects.requireNonNull(foundAsset, "Asset with ID %s not found".formatted(assetId)); - val editedAsset = assetBuilder.fromEditMetadataRequest(foundAsset, request); + val editedAsset = assetMapper.editAsset(foundAsset, request); val updatedAsset = assetService.update(editedAsset).orElseThrow(ServiceException::new); return new IdResponseDto(updatedAsset.getId()); } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java deleted file mode 100644 index 7eccc608f..000000000 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.wrapper.api.ui.pages.asset; - -import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; -import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; -import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.types.domain.asset.Asset; - -@RequiredArgsConstructor -public class AssetBuilder { - private final AssetMapper assetMapper; - private final EdcPropertyUtils edcPropertyUtils; - private final AssetIdValidator assetIdValidator; - private final SelfDescriptionService selfDescriptionService; - - /** - * Creates an {@link Asset} from a {@link UiAssetCreateRequest}. - * - * @param request {@link UiAssetCreateRequest} - * @return {@link Asset} - */ - public Asset fromCreateRequest(UiAssetCreateRequest request) { - assetIdValidator.assertValid(request.getId()); - var organizationName = selfDescriptionService.getCuratorName(); - return assetMapper.buildAsset(request, organizationName); - } - - /** - * Returns an edited copy of an {@link Asset} updated with the given {@link UiAssetEditMetadataRequest} - * - * @param asset {@link Asset} (immutable) - * @param request {@link UiAssetEditMetadataRequest} - * @return copy of {@link Asset} - */ - public Asset fromEditMetadataRequest(Asset asset, UiAssetEditMetadataRequest request) { - var createRequest = buildCreateRequest(asset, request); - var tmpAsset = fromCreateRequest(createRequest); - - // DEBT: On each EDC update, check that no field was added in - // org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder - return Asset.Builder.newInstance() - .id(asset.getId()) - .properties(tmpAsset.getProperties()) - .privateProperties(tmpAsset.getPrivateProperties()) - .dataAddress(asset.getDataAddress()) - .createdAt(asset.getCreatedAt()) - .build(); - } - - - private UiAssetCreateRequest buildCreateRequest(Asset asset, UiAssetEditMetadataRequest editRequest) { - var dataAddress = edcPropertyUtils.truncateToMapOfString(asset.getDataAddress().getProperties()); - - var createRequest = new UiAssetCreateRequest(); - createRequest.setId(asset.getId()); - createRequest.setConditionsForUse(editRequest.getConditionsForUse()); - createRequest.setCustomJsonAsString(editRequest.getCustomJsonAsString()); - createRequest.setCustomJsonLdAsString(editRequest.getCustomJsonLdAsString()); - createRequest.setDataAddressProperties(dataAddress); - createRequest.setDataCategory(editRequest.getDataCategory()); - createRequest.setDataModel(editRequest.getDataModel()); - createRequest.setDataSampleUrls(editRequest.getDataSampleUrls()); - createRequest.setDataSubcategory(editRequest.getDataSubcategory()); - createRequest.setDataUpdateFrequency(editRequest.getDataUpdateFrequency()); - createRequest.setDescription(editRequest.getDescription()); - createRequest.setGeoLocation(editRequest.getGeoLocation()); - createRequest.setGeoReferenceMethod(editRequest.getGeoReferenceMethod()); - createRequest.setKeywords(editRequest.getKeywords()); - createRequest.setLandingPageUrl(editRequest.getLandingPageUrl()); - createRequest.setLanguage(editRequest.getLanguage()); - createRequest.setLicenseUrl(editRequest.getLicenseUrl()); - createRequest.setMediaType(editRequest.getMediaType()); - createRequest.setNutsLocations(editRequest.getNutsLocations()); - createRequest.setPrivateCustomJsonAsString(editRequest.getPrivateCustomJsonAsString()); - createRequest.setPrivateCustomJsonLdAsString(editRequest.getPrivateCustomJsonLdAsString()); - createRequest.setPublisherHomepage(editRequest.getPublisherHomepage()); - createRequest.setReferenceFileUrls(editRequest.getReferenceFileUrls()); - createRequest.setReferenceFilesDescription(editRequest.getReferenceFilesDescription()); - createRequest.setSovereignLegalName(editRequest.getSovereignLegalName()); - createRequest.setTemporalCoverageFrom(editRequest.getTemporalCoverageFrom()); - createRequest.setTemporalCoverageToInclusive(editRequest.getTemporalCoverageToInclusive()); - createRequest.setTitle(editRequest.getTitle()); - createRequest.setTransportMode(editRequest.getTransportMode()); - createRequest.setVersion(editRequest.getVersion()); - - return createRequest; - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ParameterizationCompatibilityUtils.java similarity index 93% rename from extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java rename to extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ParameterizationCompatibilityUtils.java index 70e558338..0a3dd04d8 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/ParameterizationCompatibilityUtils.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ParameterizationCompatibilityUtils.java @@ -13,7 +13,7 @@ */ -package de.sovity.edc.ext.wrapper.api.common.mappers.utils; +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; import lombok.val; import org.eclipse.edc.spi.types.domain.DataAddress; @@ -37,7 +37,7 @@ public class ParameterizationCompatibilityUtils { ); public DataAddress enrich(DataAddress dataAddress, Map transferProcessProperties) { - if(transferProcessProperties == null) { + if (transferProcessProperties == null) { return dataAddress; } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java index 4f27d49d4..559eef1a2 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilder.java @@ -15,8 +15,7 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; import de.sovity.edc.ext.wrapper.api.ServiceException; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; import de.sovity.edc.utils.JsonUtils; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java index b0f7ec1de..7fa390528 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/services/OwnConnectorEndpointServiceImpl.java @@ -14,7 +14,7 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.OwnConnectorEndpointService; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.OwnConnectorEndpointService; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java index adc3bc332..0baf08c63 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java @@ -15,12 +15,15 @@ import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.UiAsset; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; -import de.sovity.edc.client.gen.model.UiAssetEditMetadataRequest; +import de.sovity.edc.client.gen.model.UiAssetEditRequest; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.ext.wrapper.TestUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.FailedMappingException; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; import de.sovity.edc.utils.jsonld.vocab.Prop; import lombok.SneakyThrows; import org.eclipse.edc.connector.spi.asset.AssetService; @@ -96,17 +99,17 @@ void assetPageSorting(AssetService assetService) { @Test void testAssetCreation(AssetService assetService) { // arrange - var dataAddressProperties = Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.BASE_URL, DATA_SINK, - Prop.Edc.PROXY_METHOD, "true", - Prop.Edc.PROXY_PATH, "true", - Prop.Edc.PROXY_QUERY_PARAMS, "true", - Prop.Edc.PROXY_BODY, "true", - - // tests that a property without a context URL will survive the JSON-LD mapping - "oauth2:tokenUrl", "https://token-url" - ); + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(DATA_SINK) + .enableMethodParameterization(true) + .enablePathParameterization(true) + .enableQueryParameterization(true) + .enableBodyParameterization(true) + .build()) + .customProperties(Map.of("oauth2:tokenUrl", "https://token-url")) + .build(); var uiAssetRequest = UiAssetCreateRequest.builder() .id("asset-1") .title("AssetTitle") @@ -132,7 +135,7 @@ void testAssetCreation(AssetService assetService) { .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) .keywords(List.of("keyword1", "keyword2")) .publisherHomepage("publisherHomepage") - .dataAddressProperties(dataAddressProperties) + .dataSource(dataSource) .customJsonAsString("{\"test\":\"value\"}") .customJsonLdAsString(""" { @@ -219,21 +222,23 @@ void testAssetCreation(AssetService assetService) { """); var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); - assertThat(assetWithDataAddress.getDataAddress().getProperties()).isEqualTo(dataAddressProperties); + assertThat(assetWithDataAddress.getDataAddress().getProperties()).containsEntry("oauth2:tokenUrl", "https://token-url"); } @Test void testEditAssetMetadata(AssetService assetService) { // arrange - var dataAddress = Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.BASE_URL, DATA_SINK, - Prop.Edc.PROXY_METHOD, "true", - Prop.Edc.PROXY_PATH, "true", - Prop.Edc.PROXY_QUERY_PARAMS, "true", - Prop.Edc.PROXY_BODY, "true", - "oauth2:tokenUrl", "https://token-url" - ); + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(DATA_SINK) + .enableMethodParameterization(true) + .enablePathParameterization(true) + .enableQueryParameterization(true) + .enableBodyParameterization(true) + .build()) + .customProperties(Map.of("oauth2:tokenUrl", "https://token-url")) + .build(); var createRequest = UiAssetCreateRequest.builder() .id("asset-1") .title("AssetTitle") @@ -259,7 +264,7 @@ void testEditAssetMetadata(AssetService assetService) { .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) .keywords(List.of("keyword1", "keyword2")) .publisherHomepage("publisherHomepage") - .dataAddressProperties(dataAddress) + .dataSource(dataSource) .customJsonAsString(""" { "test": "value" } """) @@ -272,8 +277,12 @@ void testEditAssetMetadata(AssetService assetService) { .build(); client.uiApi().createAsset(createRequest); + var dataAddressBeforeEdit = assetService.query(QuerySpec.max()) + .orElseThrow(FailedMappingException::ofFailure).toList().get(0) + .getDataAddress() + .getProperties(); - var editRequest = UiAssetEditMetadataRequest.builder() + var editRequest = UiAssetEditRequest.builder() .title("AssetTitle 2") .description("AssetDescription 2") .licenseUrl("https://license-url/2") @@ -353,20 +362,25 @@ void testEditAssetMetadata(AssetService assetService) { { "https://to-change": "new value LD" } """); - var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); - assertThat(assetWithDataAddress.getDataAddress().getProperties()).isEqualTo(dataAddress); + var dataAddressAfterEdit = assetService.query(QuerySpec.max()) + .orElseThrow(FailedMappingException::ofFailure).toList().get(0) + .getDataAddress() + .getProperties(); + assertThat(dataAddressAfterEdit).isEqualTo(dataAddressBeforeEdit); } @Test void testAssetCreation_noProxying() { // arrange - var dataAddressProperties = Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.BASE_URL, DATA_SINK - ); + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(DATA_SINK) + .build()) + .build(); var uiAssetRequest = UiAssetCreateRequest.builder() .id("asset-1") - .dataAddressProperties(dataAddressProperties) + .dataSource(dataSource) .build(); // act @@ -386,13 +400,16 @@ void testAssetCreation_noProxying() { @Test void testAssetCreation_differentDataAddressType() { // arrange - var dataAddressProperties = Map.of( + var dataSource = UiDataSource.builder() + .type(DataSourceType.CUSTOM) + .customProperties(Map.of( Prop.Edc.TYPE, "Unknown" - ); + )) + .build(); var uiAssetRequest = UiAssetCreateRequest.builder() - .id("asset-1") - .dataAddressProperties(dataAddressProperties) - .build(); + .id("asset-1") + .dataSource(dataSource) + .build(); // act var response = client.uiApi().createAsset(uiAssetRequest); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java index 66cecc07d..a4a366355 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java @@ -1,13 +1,30 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.wrapper.api.ui.pages.catalog; import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.ext.wrapper.TestUtils; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; @@ -19,14 +36,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @ExtendWith(EdcExtension.class) -public class CatalogApiTest { +class CatalogApiTest { private EdcClient client; private final String dataOfferId = "my-data-offer-2023-11"; @@ -58,6 +74,13 @@ void testDistributionKey() { } private void createAsset() { + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://a") + .build()) + .build(); + var asset = UiAssetCreateRequest.builder() .id(dataOfferId) .title("My Data Offer") @@ -67,11 +90,7 @@ private void createAsset() { .publisherHomepage("https://my-department.my-org.com/my-data-offer") .licenseUrl("https://my-department.my-org.com/my-data-offer#license") .mediaType("Media Type") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, "https://a" - )) + .dataSource(dataSource) .build(); client.uiApi().createAsset(asset); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java index a29186b0a..455eed2e0 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/TransferRequestBuilderTest.java @@ -14,8 +14,7 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.utils.ParameterizationCompatibilityUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; import lombok.val; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java index 5a04d42f4..70864b626 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java @@ -87,10 +87,11 @@ private static void createAsset(AssetService assetStore, DataAddress dataAddress var asset = Asset.Builder.newInstance() .id(assetId) .property(Prop.Dcterms.TITLE, assetName) + .dataAddress(dataAddress) .createdAt(dateFormatterToLong("2023-06-01")) .build(); - assetStore.create(asset, dataAddress); + assetStore.create(asset); } private static ContractAgreement createContractAgreement( diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java index 04860b888..efcdb4ab6 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.ext.wrapper.api.usecase; import de.sovity.edc.client.EdcClient; @@ -7,12 +21,15 @@ import de.sovity.edc.client.gen.model.CatalogFilterExpressionOperator; import de.sovity.edc.client.gen.model.CatalogQuery; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.ext.wrapper.TestUtils; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; @@ -156,25 +173,24 @@ private void buildContractDefinition(String policyId, String assetId1, String cd } private void setupAssets() { + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(TestUtils.PROTOCOL_ENDPOINT) + .build()) + .build(); + assetId1 = client.uiApi().createAsset(UiAssetCreateRequest.builder() .id("test-asset-1") .title("Test Asset 1") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, TestUtils.PROTOCOL_ENDPOINT - )) + .dataSource(dataSource) .mediaType("application/json") .build()).getId(); assetId2 = client.uiApi().createAsset(UiAssetCreateRequest.builder() .id("test-asset-2") .title("Test Asset 2") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, TestUtils.PROTOCOL_ENDPOINT - )) + .dataSource(dataSource) .mediaType("application/json") .build()).getId(); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c124e45db..573e48829 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,6 @@ postgres = "42.4.5" quarkus = "2.16.6.Final" quartz = "2.3.2" restAssured = "5.4.0" -retry = "1.5.7" shadow = "7.1.2" swagger = "1.6.12" swaggerCore = "2.2.18" @@ -221,6 +220,5 @@ openapi-generator7 = { id = "org.openapi.generator", version.ref = "openapiGener quarkus = { id = "io.quarkus", version.ref = "quarkus" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } swagger-plugin = { id = "io.swagger.core.v3.swagger-gradle-plugin", version.ref = "swaggerCore" } -retry = { id = "org.gradle.test-retry", version.ref = "retry" } jooq = { id = "nu.studer.jooq", version.ref = "jooqPlugin" } flyway = { id = "org.flywaydb.flyway", version.ref = "flywayPlugin" } diff --git a/launchers/README.md b/launchers/README.md index 02d235771..9a1f60f3d 100644 --- a/launchers/README.md +++ b/launchers/README.md @@ -1,16 +1,16 @@
      - + Logo

      sovity Community Edition EDC:
      Docker Images

      - Report Bug + Report Bug · - Request Feature + Request Feature

      @@ -44,7 +44,7 @@ Our sovity Community Edition EDC is built as several docker image variants in di
      @@ -134,27 +133,24 @@ Our sovity Community Edition EDC is built as several docker image variants in di - + diff --git a/launchers/connectors/broker-server-ce/build.gradle.kts b/launchers/connectors/broker-server-ce/build.gradle.kts deleted file mode 100644 index dff7d6313..000000000 --- a/launchers/connectors/broker-server-ce/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - `java-library` - id("application") - alias(libs.plugins.shadow) -} - -dependencies { - // Control-Plane - implementation(libs.edc.controlPlaneCore) - implementation(libs.edc.dataPlaneSelectorCore) - implementation(libs.edc.apiObservability) - implementation(libs.edc.configurationFilesystem) - implementation(libs.edc.controlPlaneAggregateServices) - implementation(libs.edc.http) - implementation(libs.edc.dsp) - implementation(libs.edc.jsonLd) - - // JDK Logger - implementation(libs.edc.monitorJdkLogger) - - // Broker Server + PostgreSQL + Flyway - implementation(project(":extensions:broker-server")) - - implementation(libs.edc.vaultFilesystem) - implementation(libs.edc.oauth2Core) -} - -application { - mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") -} - -tasks.withType { - mergeServiceFiles() - archiveFileName.set("app.jar") -} diff --git a/launchers/connectors/broker-server-dev/build.gradle.kts b/launchers/connectors/broker-server-dev/build.gradle.kts deleted file mode 100644 index 89df7cb7e..000000000 --- a/launchers/connectors/broker-server-dev/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - `java-library` - id("application") - alias(libs.plugins.shadow) -} - -dependencies { - // Control-Plane - implementation(libs.edc.controlPlaneCore) - implementation(libs.edc.dataPlaneSelectorCore) - implementation(libs.edc.apiObservability) - implementation(libs.edc.configurationFilesystem) - implementation(libs.edc.controlPlaneAggregateServices) - implementation(libs.edc.http) - implementation(libs.edc.dsp) - implementation(libs.edc.jsonLd) - - // JDK Logger - implementation(libs.edc.monitorJdkLogger) - - // Broker Server + PostgreSQL + Flyway - implementation(project(":extensions:broker-server")) - - // Connector-To-Connector IAM - implementation(libs.edc.iamMock) -} - -application { - mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") -} - -tasks.withType { - mergeServiceFiles() - archiveFileName.set("app.jar") -} diff --git a/launchers/connectors/catalog-crawler-ce/build.gradle.kts b/launchers/connectors/catalog-crawler-ce/build.gradle.kts new file mode 100644 index 000000000..e72aa1f52 --- /dev/null +++ b/launchers/connectors/catalog-crawler-ce/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `java-library` + id("application") + alias(libs.plugins.shadow) +} + +dependencies { + implementation(project(":extensions:catalog-crawler:catalog-crawler-launcher-base")) + + api(libs.edc.monitorJdkLogger) + api(libs.edc.apiObservability) + + implementation(project(":launchers:common:auth-daps")) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} diff --git a/launchers/connectors/catalog-crawler-dev/build.gradle.kts b/launchers/connectors/catalog-crawler-dev/build.gradle.kts new file mode 100644 index 000000000..c68026e6c --- /dev/null +++ b/launchers/connectors/catalog-crawler-dev/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `java-library` + id("application") + alias(libs.plugins.shadow) +} + +dependencies { + implementation(project(":extensions:catalog-crawler:catalog-crawler-launcher-base")) + + api(libs.edc.monitorJdkLogger) + api(libs.edc.apiObservability) + + implementation(project(":launchers:common:auth-mock")) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("app.jar") +} diff --git a/launchers/connectors/sovity-dev/build.gradle.kts b/launchers/connectors/sovity-dev/build.gradle.kts index c1d619f2c..c65dd86ff 100644 --- a/launchers/connectors/sovity-dev/build.gradle.kts +++ b/launchers/connectors/sovity-dev/build.gradle.kts @@ -19,4 +19,5 @@ tasks.withType { archiveFileName.set("app.jar") } + group = libs.versions.sovityEdcGroup.get() diff --git a/settings.gradle.kts b/settings.gradle.kts index 7f9ba06d0..bb5b28436 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,16 @@ -rootProject.name = "edc-ce" +rootProject.name = "sovity-ce-edc" -include(":connector") -include(":extensions:broker-server") -include(":extensions:broker-server-api:api") -include(":extensions:broker-server-api:client") -include(":extensions:broker-server-postgres-flyway-jooq") +include(":extensions:catalog-crawler:catalog-crawler") +include(":extensions:catalog-crawler:catalog-crawler-db") +include(":extensions:catalog-crawler:catalog-crawler-launcher-base") +include(":extensions:catalog-crawler:catalog-crawler-e2e-test") include(":extensions:edc-ui-config") include(":extensions:last-commit-info") include(":extensions:policy-always-true") include(":extensions:policy-referring-connector") include(":extensions:policy-time-interval") include(":extensions:postgres-flyway") +include(":extensions:postgres-flyway-core") include(":extensions:sovity-messenger") include(":extensions:sovity-edc-extensions-package") include(":extensions:test-backend-controller") @@ -25,10 +25,11 @@ include(":extensions:wrapper:wrapper-ee-api") include(":launchers:common:auth-daps") include(":launchers:common:auth-mock") include(":launchers:common:base") +include(":launchers:common:base-catalog-crawler") include(":launchers:common:base-mds") include(":launchers:common:observability") -include(":launchers:connectors:broker-server-ce") -include(":launchers:connectors:broker-server-dev") +include(":launchers:connectors:catalog-crawler-ce") +include(":launchers:connectors:catalog-crawler-dev") include(":launchers:connectors:mds-ce") include(":launchers:connectors:sovity-ce") include(":launchers:connectors:sovity-dev") diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java index 6c7a046f8..a4e3d76a4 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java @@ -54,7 +54,7 @@ public static ConnectorConfig forTestDatabase(String participantId, TestDatabase return config; } - private static synchronized int getFreePortRange(int size) { + public static synchronized int getFreePortRange(int size) { // pick a random in a reasonable range int firstPort = getFreePort(RANDOM.nextInt(10_000, 50_000)); diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java new file mode 100644 index 000000000..91096b7b8 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java @@ -0,0 +1,36 @@ +package de.sovity.edc.extension.e2e.db; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +import java.util.Map; +import java.util.function.Supplier; + +@RequiredArgsConstructor +public class EdcRuntimeExtensionDeferred + implements BeforeAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { + + private final String moduleName; + private final String logPrefix; + + private final Supplier> propertyFactory; + + @Delegate(types = { + BeforeTestExecutionCallback.class, + AfterTestExecutionCallback.class, + ParameterResolver.class + }) + @Getter + private EdcRuntimeExtensionFixed edcRuntimeExtensionFixed = null; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + edcRuntimeExtensionFixed = new EdcRuntimeExtensionFixed(moduleName, logPrefix, propertyFactory.get()); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java new file mode 100644 index 000000000..d344b5f00 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements + * + */ + +package de.sovity.edc.extension.e2e.db; + +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.eclipse.edc.boot.system.ExtensionLoader.loadMonitor; + +/** + * A JUnit extension for running an embedded EDC runtime as part of a test fixture. A custom gradle task printClasspath + * is used to determine the runtime classpath of the module to run. The runtime obtains a classpath determined by the + * Gradle build. + *

      + * This extension attaches an EDC runtime to the {@link BeforeTestExecutionCallback} and + * {@link AfterTestExecutionCallback} lifecycle hooks. Parameter injection of runtime services is supported. + */ +public class EdcRuntimeExtensionFixed extends EdcExtension { + private static final Monitor MONITOR = loadMonitor(); + + private final String moduleName; + private final String logPrefix; + private final Map properties; + private Thread runtimeThread; + + public EdcRuntimeExtensionFixed(String moduleName, String logPrefix, Map properties) { + this.moduleName = moduleName; + this.logPrefix = logPrefix; + this.properties = Map.copyOf(properties); + } + + @Override + public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + // Find the project root directory, moving up the directory tree + var root = TestUtils.findBuildRoot(); + + // Run a Gradle custom task to determine the runtime classpath of the module to run + String[] command = { + new File(root, TestUtils.GRADLE_WRAPPER).getCanonicalPath(), + "-q", + moduleName + ":printClasspath" + }; + Process exec = Runtime.getRuntime().exec(command); + var classpathString = new String(exec.getInputStream().readAllBytes()); + var errorOutput = new String(exec.getErrorStream().readAllBytes()); + if (exec.waitFor() != 0) { + throw new EdcException(format("Failed to run gradle command: [%s]. Output: %s %s", + String.join(" ", command), classpathString, errorOutput)); + } + + // Replace subproject JAR entries with subproject build directories in classpath. + // This ensures modified classes are picked up without needing to rebuild dependent JARs. + + var splitRegex = ":|\\s"; + if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows")) { + splitRegex = ";|\\s"; + } + + var classPathEntries = Arrays.stream(classpathString.split(splitRegex)) + .filter(s -> !s.isBlank()) + .flatMap(p -> resolveClassPathEntry(root, p)) + .toArray(URL[]::new); + + // Create a ClassLoader that only has the target module class path, and is not + // parented with the current ClassLoader. + var classLoader = URLClassLoader.newInstance(classPathEntries, ClassLoader.getSystemClassLoader()); + + // Temporarily inject system properties. + var savedProperties = (Properties) System.getProperties().clone(); + properties.forEach(System::setProperty); + + var latch = new CountDownLatch(1); + + runtimeThread = new Thread(() -> { + try { + + // Make the ClassLoader available to the ServiceLoader. + // This ensures the target module's extensions are discovered and loaded at runtime boot. + Thread.currentThread().setContextClassLoader(classLoader); + + // Boot EDC runtime. + super.beforeTestExecution(extensionContext); + + latch.countDown(); + } catch (Exception e) { + throw new EdcException(e); + } + }); + + MONITOR.info("Starting module " + moduleName); + // Start thread and wait for EDC to start up. + runtimeThread.start(); + + if (!latch.await(20, SECONDS)) { + throw new EdcException("Failed to start EDC runtime"); + } + + MONITOR.info("Module " + moduleName + " started"); + // Restore system properties. + System.setProperties(savedProperties); + } + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + if (runtimeThread != null) { + runtimeThread.join(); + } + super.afterTestExecution(context); + } + + @Override + protected @NotNull Monitor createMonitor() { + // disable logs when "quiet" log level is set + if (System.getProperty("org.gradle.logging.level") != null) { + return new Monitor() { + }; + } else { + return new ConsoleMonitor(logPrefix, ConsoleMonitor.Level.DEBUG); + } + } + + /** + * Replace Gradle subproject JAR entries with subproject build directories in classpath. This ensures modified + * classes are picked up without needing to rebuild dependent JARs. + * + * @param root project root directory. + * @param classPathEntry class path entry to resolve. + * @return resolved class path entries for the input argument. + */ + private Stream resolveClassPathEntry(File root, String classPathEntry) { + try { + File f = new File(classPathEntry).getCanonicalFile(); + + // If class path entry is not a JAR under the root (i.e. a sub-project), do not transform it + boolean isUnderRoot = f.getCanonicalPath().startsWith(root.getCanonicalPath() + File.separator); + if (!classPathEntry.toLowerCase(Locale.ROOT).endsWith(".jar") || !isUnderRoot) { + var sanitizedClassPathEntry = classPathEntry.replace("build/resources/main", "src/main/resources"); + return Stream.of(new File(sanitizedClassPathEntry).toURI().toURL()); + } + + // Replace JAR entry with the resolved classes and resources folder + var buildDir = f.getParentFile().getParentFile(); + return Stream.of( + new File(buildDir, "/classes/java/main").toURI().toURL(), + new File(buildDir, "../src/main/resources").toURI().toURL() + ); + } catch (IOException e) { + throw new EdcException(e); + } + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java new file mode 100644 index 000000000..af15b7822 --- /dev/null +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java @@ -0,0 +1,43 @@ +package de.sovity.edc.extension.e2e.db; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +import java.util.Map; +import java.util.function.Function; + +@RequiredArgsConstructor +public class EdcRuntimeExtensionWithTestDatabase + implements BeforeAllCallback, AfterAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { + + private final String moduleName; + private final String logPrefix; + + @Getter + @Delegate(types = {AfterAllCallback.class}) + private final TestDatabase testDatabase = new TestDatabaseViaTestcontainers(); + + private final Function> propertyFactory; + + @Delegate(types = { + BeforeTestExecutionCallback.class, + AfterTestExecutionCallback.class, + ParameterResolver.class + }) + @Getter + private EdcRuntimeExtensionFixed edcRuntimeExtension = null; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + testDatabase.beforeAll(extensionContext); + edcRuntimeExtension = new EdcRuntimeExtensionFixed(moduleName, logPrefix, propertyFactory.apply(testDatabase)); + } +} From 2db950efedd2f543a2a3bbaddd895ffecf9d7b21 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Tue, 9 Jul 2024 14:15:13 +0200 Subject: [PATCH 258/295] feat: terminatedBy for contract termination API (#992) --- docs/api/sovity-edc-api-wrapper.yaml | 29 ++++++++++++------- .../ContractAgreementTerminationInfo.java | 6 ++++ .../api/ui/model/ContractTerminatedBy.java | 22 ++++++++++++++ .../ui/model/ContractTerminationStatus.java | 3 ++ 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index c5531ca25..13d5135d1 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -1323,11 +1323,7 @@ components: items: $ref: '#/components/schemas/ContractAgreementTransferProcess' terminationStatus: - type: string - description: Contract Agreement's Termination Status - enum: - - ONGOING - - TERMINATED + $ref: '#/components/schemas/ContractTerminationStatus' terminationInformation: $ref: '#/components/schemas/ContractAgreementTerminationInfo' description: Contract Agreement for Contract Agreement Page @@ -1353,6 +1349,7 @@ components: - detail - reason - terminatedAt + - terminatedBy type: object properties: terminatedAt: @@ -1367,6 +1364,8 @@ components: type: string description: Detailed message from the terminating party about why the contract was terminated. + terminatedBy: + $ref: '#/components/schemas/ContractTerminatedBy' description: Contract's agreement metadata ContractAgreementTransferProcess: required: @@ -1388,6 +1387,19 @@ components: type: string description: Error Message description: A Contract Agreement's Transfer Process + ContractTerminatedBy: + type: string + description: Whether the contract termination was initiated by this EDC or a + counterparty EDC. + enum: + - SELF + - COUNTERPARTY + ContractTerminationStatus: + type: string + description: The contract termination status + enum: + - ONGOING + - TERMINATED TransferProcessSimplifiedState: type: string description: Simplified Transfer Process State to be used in UI @@ -1417,12 +1429,7 @@ components: type: object properties: terminationStatus: - type: string - description: Optionally filter the resulting contract agreements by their - termination status. - enum: - - ONGOING - - TERMINATED + $ref: '#/components/schemas/ContractTerminationStatus' description: Filters for querying a Contract Contract Agreement Page ContractDefinitionEntry: required: diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java index d96223ef7..0f7cf243e 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java @@ -40,4 +40,10 @@ public class ContractAgreementTerminationInfo { requiredMode = Schema.RequiredMode.REQUIRED ) private String detail; + + @Schema( + description = "Indicates whether the termination comes from this EDC or the counterparty EDC.", + requiredMode = Schema.RequiredMode.REQUIRED + ) + private ContractTerminatedBy terminatedBy; } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java new file mode 100644 index 000000000..f249a3869 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Whether the contract termination was initiated by this EDC or a counterparty EDC.", enumAsRef = true) +public enum ContractTerminatedBy { + SELF, + COUNTERPARTY +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java index b802b60ba..652c18364 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java @@ -13,6 +13,9 @@ package de.sovity.edc.ext.wrapper.api.ui.model; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "The contract termination status", enumAsRef = true) public enum ContractTerminationStatus { ONGOING, TERMINATED From 2bbdf1ba96287481a99fa1008d180988591003bb Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 10 Jul 2024 12:45:12 +0300 Subject: [PATCH 259/295] fix: crawler not handling descriptions properly (#993) --- .../migration/V9__Broker_Integration.sql | 32 ++-- .../CrawlerExtensionContextBuilder.java | 3 +- .../fetching/model/FetchedCatalog.java | 6 + .../fetching/model/FetchedContractOffer.java | 6 + .../fetching/model/FetchedDataOffer.java | 6 + .../data_offers/DataOfferRecordUpdater.java | 17 +- .../ext/catalog/crawler/CrawlerTestDb.java | 1 - .../writing/ConnectorSuccessWriterTest.java | 146 ++++++++++++++++++ .../writing/DataOfferWriterTestDydi.java | 19 ++- .../asset/utils/ShortDescriptionBuilder.java | 4 +- 10 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorSuccessWriterTest.java diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql index fe46c6525..a93987578 100644 --- a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql @@ -32,22 +32,22 @@ alter table connector -- Data offers, additionally keyed by env ID create table data_offer ( - connector_id text not null, - asset_id text not null, - ui_asset_json jsonb not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone, - asset_title text collate alphanumeric_with_natural_sort not null, - description text not null default ''::text, - curator_organization_name text not null default ''::text, - data_category text not null default ''::text, - data_subcategory text not null default ''::text, - data_model text not null default ''::text, - transport_mode text not null default ''::text, - geo_reference_method text not null default ''::text, - keywords text[] not null default '{}'::text[], - keywords_comma_joined text not null default ''::text, - version text not null default ''::text, + connector_id text not null, + asset_id text not null, + ui_asset_json jsonb not null, + created_at timestamp with time zone not null, + updated_at timestamp with time zone, + asset_title text collate alphanumeric_with_natural_sort not null, + description_no_markdown text not null default ''::text, + short_description_no_markdown text not null default ''::text, + data_category text not null default ''::text, + data_subcategory text not null default ''::text, + data_model text not null default ''::text, + transport_mode text not null default ''::text, + geo_reference_method text not null default ''::text, + keywords text[] not null default '{}'::text[], + keywords_comma_joined text not null default ''::text, + version text not null default ''::text, primary key (connector_id, asset_id), constraint data_offer_connector_fkey foreign key (connector_id) references connector (connector_id) ); diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java index cb49d40a5..2437dd751 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java @@ -129,7 +129,8 @@ public static CrawlerExtensionContext buildContext( objectMapperJsonLd ); var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); - var dataOfferRecordUpdater = new DataOfferRecordUpdater(connectorQueries); + var shortDescriptionBuilder = new ShortDescriptionBuilder(); + var dataOfferRecordUpdater = new DataOfferRecordUpdater(shortDescriptionBuilder); var contractOfferQueries = new ContractOfferQueries(); var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(crawlerConfig, crawlerEventLogger); var dataOfferPatchBuilder = new CatalogPatchBuilder( diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java index d930eb223..027d2c8b3 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java @@ -16,7 +16,10 @@ import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; @@ -27,6 +30,9 @@ */ @Getter @Setter +@Builder +@RequiredArgsConstructor +@AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedCatalog { ConnectorRef connectorRef; diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java index 64317498e..2b1993749 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java @@ -15,12 +15,18 @@ package de.sovity.edc.ext.catalog.crawler.crawling.fetching.model; import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; @Getter @Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedContractOffer { String contractOfferId; diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java index 752fc98fd..a2e6fe99b 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java @@ -16,7 +16,10 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; @@ -27,6 +30,9 @@ */ @Getter @Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedDataOffer { String assetId; diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java index 955c05271..c09a3d0b8 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java @@ -16,11 +16,11 @@ import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; import de.sovity.edc.ext.catalog.crawler.crawling.writing.utils.ChangeTracker; -import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; import de.sovity.edc.ext.catalog.crawler.dao.utils.JsonbUtils; import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; import de.sovity.edc.ext.catalog.crawler.utils.JsonUtils2; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; import lombok.RequiredArgsConstructor; import org.jooq.JSONB; @@ -36,8 +36,7 @@ */ @RequiredArgsConstructor public class DataOfferRecordUpdater { - - private final ConnectorQueries connectorQueries; + private final ShortDescriptionBuilder shortDescriptionBuilder; /** * Create a new {@link DataOfferRecord}. @@ -84,9 +83,15 @@ public boolean updateDataOffer( ); changes.setIfChanged( - blankIfNull(record.getDescription()), - blankIfNull(asset.getDescription()), - record::setDescription + blankIfNull(record.getDescriptionNoMarkdown()), + shortDescriptionBuilder.extractMarkdownText(blankIfNull(asset.getDescription())), + record::setDescriptionNoMarkdown + ); + + changes.setIfChanged( + blankIfNull(record.getShortDescriptionNoMarkdown()), + blankIfNull(asset.getDescriptionShortText()), + record::setShortDescriptionNoMarkdown ); changes.setIfChanged( diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java index 7e7026048..a4057c57b 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java @@ -17,7 +17,6 @@ public class CrawlerTestDb implements BeforeAllCallback, AfterAllCallback { private final TestDatabaseViaTestcontainers db = new TestDatabaseViaTestcontainers(); - private HikariDataSource dataSource = null; private DslContextFactory dslContextFactory = null; diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorSuccessWriterTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorSuccessWriterTest.java new file mode 100644 index 000000000..4e7d6a603 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorSuccessWriterTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.CrawlerTestDb; +import de.sovity.edc.ext.catalog.crawler.TestData; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedCatalog; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import org.assertj.core.data.TemporalUnitLessThanOffset; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +class ConnectorSuccessWriterTest { + @RegisterExtension + private static final CrawlerTestDb TEST_DATABASE = new CrawlerTestDb(); + + ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter; + + @BeforeEach + void setup() { + var container = new DataOfferWriterTestDydi(); + connectorUpdateSuccessWriter = container.getConnectorUpdateSuccessWriter(); + when(container.getCrawlerConfig().getMaxContractOffersPerDataOffer()).thenReturn(1); + when(container.getCrawlerConfig().getMaxDataOffersPerConnector()).thenReturn(1); + } + + @Test + void testDataOfferWriter_fullSingleUpdate() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + var connectorRef = TestData.connectorRef; + TestData.insertConnector(dsl, connectorRef, unused -> { + }); + var uiAsset = UiAsset.builder() + .assetId("assetId") + .title("title") + .description("# Description\n\n**with Markdown**") + .descriptionShortText("descriptionShortText") + .dataCategory("dataCategory") + .dataSubcategory("dataSubCategory") + .dataModel("dataModel") + .transportMode("transportMode") + .geoReferenceMethod("geoReferenceMethod") + .keywords(List.of("a", "b")) + .build(); + var fetchedContractOffer = FetchedContractOffer.builder() + .contractOfferId("contractOfferId") + .uiPolicyJson("\"test-policy\"") + .build(); + var fetchedDataOffer = FetchedDataOffer.builder() + .assetId("assetId") + .uiAsset(uiAsset) + .uiAssetJson("\"test\"") + .contractOffers(List.of(fetchedContractOffer)) + .build(); + var fetchedCatalog = FetchedCatalog.builder() + .connectorRef(connectorRef) + .dataOffers(List.of(fetchedDataOffer)) + .build(); + + // act + connectorUpdateSuccessWriter.handleConnectorOnline( + dsl, + connectorRef, + dsl.fetchOne( + Tables.CONNECTOR, + Tables.CONNECTOR.CONNECTOR_ID.eq(connectorRef.getConnectorId()) + ), + fetchedCatalog + ); + + // assert + var connector = dsl.fetchOne( + Tables.CONNECTOR, + Tables.CONNECTOR.CONNECTOR_ID.eq(connectorRef.getConnectorId()) + ); + var dataOffer = dsl.fetchOne( + Tables.DATA_OFFER, + DSL.and( + Tables.DATA_OFFER.CONNECTOR_ID.eq(connectorRef.getConnectorId()), + Tables.DATA_OFFER.ASSET_ID.eq("assetId") + ) + ); + var contractOffer = dsl.fetchOne( + Tables.CONTRACT_OFFER, + DSL.and( + Tables.CONTRACT_OFFER.CONNECTOR_ID.eq(connectorRef.getConnectorId()), + Tables.CONTRACT_OFFER.ASSET_ID.eq("assetId"), + Tables.CONTRACT_OFFER.CONTRACT_OFFER_ID.eq("contractOfferId") + ) + ); + + var now = OffsetDateTime.now(); + var minuteAccuracy = new TemporalUnitLessThanOffset(1, ChronoUnit.MINUTES); + assertThat(connector).isNotNull(); + assertThat(connector.getOnlineStatus()).isEqualTo(ConnectorOnlineStatus.ONLINE); + assertThat(connector.getLastRefreshAttemptAt()).isCloseTo(now, minuteAccuracy); + assertThat(connector.getLastSuccessfulRefreshAt()).isCloseTo(now, minuteAccuracy); + assertThat(connector.getDataOffersExceeded()).isEqualTo(ConnectorDataOffersExceeded.OK); + assertThat(connector.getContractOffersExceeded()).isEqualTo(ConnectorContractOffersExceeded.OK); + + assertThat(dataOffer).isNotNull(); + assertThat(dataOffer.getAssetTitle()).isEqualTo("title"); + assertThat(dataOffer.getDescriptionNoMarkdown()).isEqualTo("Description with Markdown"); + assertThat(dataOffer.getShortDescriptionNoMarkdown()).isEqualTo("descriptionShortText"); + assertThat(dataOffer.getDataCategory()).isEqualTo("dataCategory"); + assertThat(dataOffer.getDataSubcategory()).isEqualTo("dataSubCategory"); + assertThat(dataOffer.getDataModel()).isEqualTo("dataModel"); + assertThat(dataOffer.getTransportMode()).isEqualTo("transportMode"); + assertThat(dataOffer.getGeoReferenceMethod()).isEqualTo("geoReferenceMethod"); + assertThat(dataOffer.getKeywords()).containsExactly("a", "b"); + assertThat(dataOffer.getKeywordsCommaJoined()).isEqualTo("a, b"); + assertThat(dataOffer.getUiAssetJson().data()).isEqualTo("\"test\""); + + assertThat(contractOffer).isNotNull(); + assertThat(contractOffer.getUiPolicyJson().data()).isEqualTo("\"test-policy\""); + }); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java index fb39bcbec..a46cf1dc4 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.catalog.crawler.crawling.writing; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; import de.sovity.edc.ext.catalog.crawler.dao.CatalogPatchApplier; import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferQueries; @@ -21,6 +22,7 @@ import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferQueries; import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferRecordUpdater; import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; import lombok.Value; import org.eclipse.edc.spi.system.configuration.Config; @@ -34,9 +36,8 @@ class DataOfferWriterTestDydi { ContractOfferQueries contractOfferQueries = new ContractOfferQueries(); ContractOfferRecordUpdater contractOfferRecordUpdater = new ContractOfferRecordUpdater(); ConnectorQueries connectorQueries = new ConnectorQueries(crawlerConfig); - DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater( - connectorQueries - ); + ShortDescriptionBuilder shortDescriptionBuilder = new ShortDescriptionBuilder(); + DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater(shortDescriptionBuilder); CatalogPatchBuilder catalogPatchBuilder = new CatalogPatchBuilder( contractOfferQueries, dataOfferQueries, @@ -45,4 +46,16 @@ class DataOfferWriterTestDydi { ); CatalogPatchApplier catalogPatchApplier = new CatalogPatchApplier(); ConnectorUpdateCatalogWriter connectorUpdateCatalogWriter = new ConnectorUpdateCatalogWriter(catalogPatchBuilder, catalogPatchApplier); + + // for the ConnectorUpdateSuccessWriterTest + CrawlerEventLogger crawlerEventLogger = new CrawlerEventLogger(); + DataOfferLimitsEnforcer dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer( + crawlerConfig, + crawlerEventLogger + ); + ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter( + crawlerEventLogger, + connectorUpdateCatalogWriter, + dataOfferLimitsEnforcer + ); } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java index 0d85792dd..1f59e9960 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java @@ -25,11 +25,11 @@ public String buildShortDescription(String descriptionMarkdown) { return null; } - var text = extractText(descriptionMarkdown); + var text = extractMarkdownText(descriptionMarkdown); return abbreviate(text, 300); } - String extractText(String markdown) { + public String extractMarkdownText(String markdown) { var options = new MutableDataSet(); var parser = Parser.builder(options).build(); var renderer = HtmlRenderer.builder(options).build(); From da2edd785acdc2a0adfd78a4804bdc170fc44951 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 10 Jul 2024 15:03:25 +0300 Subject: [PATCH 260/295] editorconfig: continuation indent 8 -> 4 (TODO: apply on project) (#995) --- .editorconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 41861c3d0..c79ad8243 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,7 +24,6 @@ indent_size = 2 [*.java] max_line_length = 140 -ij_continuation_indent_size = 8 ij_java_blank_lines_after_imports = 1 ij_java_blank_lines_after_package = 1 ij_java_line_comment_add_space = true From 6cf64dd047d0045510b233692a08d9b9d8ee8ae9 Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:02:35 +0200 Subject: [PATCH 261/295] chore: sync crawler db migration with ap (#997) --- .../resources/db-crawler/migration/V9__Broker_Integration.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql index a93987578..25b2e4ca3 100644 --- a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql @@ -104,4 +104,4 @@ create table crawler_execution_time_measurement error_status measurement_error_status not null ); - +alter type component_type add value 'CATALOG_CRAWLER'; From 602355f3d880f3b9fed4449f11bb98654196a776 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 15 Jul 2024 10:01:46 +0300 Subject: [PATCH 262/295] chore: prepare release (#999) --- .env | 6 +- .github/ISSUE_TEMPLATE/release.md | 5 +- CHANGELOG.md | 50 ++++- SUMMARY.md | 2 - .../catalog-crawler-production/README.md | 2 +- .../goals/development/README.md | 2 +- .../goals/local-demo/4.2.0/README.md | 63 ------ .../goals/local-demo/README.md | 4 +- .../goals/production/4.2.0/README.md | 182 ------------------ .../production/4.2.0/public-endpoints.yaml | 30 --- .../goals/production/README.md | 2 - extensions/catalog-crawler/README.md | 7 +- .../api/usecase/UseCaseApiWrapperTest.java | 2 +- 13 files changed, 61 insertions(+), 296 deletions(-) delete mode 100644 docs/deployment-guide/goals/local-demo/4.2.0/README.md delete mode 100644 docs/deployment-guide/goals/production/4.2.0/README.md delete mode 100644 docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml diff --git a/.env b/.env index e415c2283..1d0ed94d1 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:8.1.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:8.1.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 +EDC_IMAGE=ghcr.io/sovity/edc-dev:9.0.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:9.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.0.0 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 466f65ea1..66e703df3 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -32,14 +32,15 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Add a clean `Unreleased` version. - [ ] Add the version to the old section. - [ ] Add the current date to the old version. + - [ ] Add all relevant changelog entries from the newer EDC UI release(s), merge and reword them. + - [ ] Add all deployment migration notes from the newer EDC UI release(s), merge and reword them. - [ ] Check the commit history for commits that might be product-relevant and thus should be added to the changelog. Maybe they were forgotten. - [ ] Write or review the `Deployment Migration Notes` section, check the commit history for changed / added configuration properties. + - [ ] Reorder, reword or combine changelog entries from a product perspective for consistency. - [ ] Write or review a release summary. - [ ] Write or review the compatible versions section. - - [ ] Add a link to the EDC UI Release to the "EDC UI" section. - - [ ] Add a link to the EDC UI Release Deployment Migration Notes from the Deployment Migration section if the EDC UI has Deployment Migration Notes. - [ ] Remove empty sections from the patch notes. - [ ] Replace the existing `docker-compose.yaml` with `docker-compose-dev.yaml`. - [ ] Set the version for `EDC_IMAGE` of diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b4f7b35..4c677656c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,46 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Major Changes -- The Broker has been removed in favor of the Authority Portal and the new Deployment Unit, the "Data Catalog Crawler": +#### Minor Changes + +#### Patch Changes + +### Deployment Migration Notes + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:{{ VERSION }}` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:{{ VERSION }}` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:{{ VERSION }}` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:{{ VERSION }}` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` + +## [9.0.0] - 2024-07-15 + +### Overview + +MDS 2.2 intermediate release + +### Detailed Changes + +#### Major Changes + +- API Wrapper UI API: Data sources are now well-typed. +- The Broker has been removed in favor of the Authority Portal: + - A new Deployment Unit, the ["Data Catalog Crawler"](extensions/catalog-crawler/README.md), has been added. - Each "Data Catalog Crawler" connects to an existing Authority Portal Deployment's DB. - Each "Data Catalog Crawler" is responsible for crawling exactly one environment. - The Data Catalog functionality of the Broker has been integrated into the Authority Portal. -- API Wrapper UI API: Moved to well-typed data sources, breaking changes to the asset model and API. #### Minor Changes -- Add the SovityMessenger extension +- Additional ToS check during contract negotiation via the UI. +- "On Request" Data Offers + - Full support in the API Wrapper UI API + - Create support in the Connector UI. Full support in the UI is still in progress. +- Added the `sovity-messenger` extension for easy Connector-to-Connector messages. #### Patch Changes @@ -28,14 +59,23 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Deployment Migration Notes - Connector: - - The database migration system has been moved from multiple migration history tables to a single one. Although this - process has been extensively tested in the enterprise edition already, it should be tested once more on a copy of a productive connector. + - The database migration system has been moved from multiple migration history tables to a single one. - Broker: - The broker has been removed. For Authority Portal users, please check out the new [Data Catalog Crawler Productive Deployment Guide](docs/deployment-guide/goals/catalog-crawler-production/README.md). - Any previous broker deployment's database is not required anymore. - Please care that only some environment variables look similar. It is recommended to create fresh deployments. +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:9.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:9.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:9.0.0` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:9.0.0` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:9.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.0.0` + ## [8.1.0] - 2024-06-14 ### Overview diff --git a/SUMMARY.md b/SUMMARY.md index ea8baafc3..1f70b94fc 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -19,8 +19,6 @@ * [Deployment Goal: Local Demo](./docs/deployment-guide/goals/local-demo) * [Deployment Goal: Development](./docs/deployment-guide/goals/development) * [Deployment Goal: Production](./docs/deployment-guide/goals/production) - * [Productive Deployment Guide](./docs/deployment-guide/goals/production) - * [Productive Deployment Guide 4.2.0 / MS8 / MDS 1.2](docs/deployment-guide/goals/production/4.2.0/README.md) ## Developer Documentation diff --git a/docs/deployment-guide/goals/catalog-crawler-production/README.md b/docs/deployment-guide/goals/catalog-crawler-production/README.md index 5bbbddecf..2136c6bc6 100644 --- a/docs/deployment-guide/goals/catalog-crawler-production/README.md +++ b/docs/deployment-guide/goals/catalog-crawler-production/README.md @@ -51,5 +51,5 @@ EDC_OAUTH_CERTIFICATE_ALIAS: 1 EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 ``` -All pre-configured config values for either the catalog crawler can be found +Additional available configuration options can be found in [launcher/.env.catalog-crawler](../../../../launchers/.env.catalog-crawler). diff --git a/docs/deployment-guide/goals/development/README.md b/docs/deployment-guide/goals/development/README.md index b8af69d87..09239430c 100644 --- a/docs/deployment-guide/goals/development/README.md +++ b/docs/deployment-guide/goals/development/README.md @@ -16,7 +16,7 @@ the [docker-compose-dev.yaml](../../../../docker-compose-dev.yaml), execute:

      - + diff --git a/docs/deployment-guide/goals/local-demo/4.2.0/README.md b/docs/deployment-guide/goals/local-demo/4.2.0/README.md deleted file mode 100644 index 9880da6a9..000000000 --- a/docs/deployment-guide/goals/local-demo/4.2.0/README.md +++ /dev/null @@ -1,63 +0,0 @@ -Deployment Goal: Local Demo -======== -> This is for an old major version sovity EDC CE 4.2.0. [Go back](../README.md) - -> On how to deploy a productive connector with joining an existing Data Space, please refer -> to our [Productive Deployment Guide](../../production/4.2.0/README.md). - -## Quick Start - -To quickly start using our sovity EDC CE or MDS EDC CE, we offer a quick -start [docker-compose.yaml](https://github.com/sovity/edc-ce/blob/v4.2.0/docker-compose.yaml) file. - -
      - edc-dev + edc-dev Development @@ -65,7 +65,7 @@ Our sovity Community Edition EDC is built as several docker image variants in di
      - edc-ce + edc-ce sovity Community Edition @@ -85,7 +85,7 @@ Our sovity Community Edition EDC is built as several docker image variants in di
      - edc-ce-mds + edc-ce-mds MDS Community Edition diff --git a/settings.gradle.kts b/settings.gradle.kts index a3ac33a58..7f9ba06d0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "edc-extensions" +rootProject.name = "edc-ce" include(":connector") include(":extensions:broker-server") diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 9c6c28473..afd26a2d6 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -1,6 +1,5 @@ plugins { `java-library` - alias(libs.plugins.retry) } dependencies { @@ -22,8 +21,4 @@ dependencies { testRuntimeOnly(libs.junit.engine) } -tasks.withType { - maxParallelForks = 1 -} - group = libs.versions.sovityEdcGroup.get() diff --git a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java index dd0a9916c..1c946456a 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java @@ -17,6 +17,7 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; @@ -28,6 +29,8 @@ import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.client.gen.model.UiPolicyLiteral; @@ -45,7 +48,6 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Map; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; @@ -121,6 +123,13 @@ void provide_and_consume() { } private void createAsset() { + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(dataOfferData)) + .build()) + .build(); + var asset = UiAssetCreateRequest.builder() .id(dataOfferId) .title("My Data Offer") @@ -129,11 +138,7 @@ private void createAsset() { .language("EN") .publisherHomepage("https://my-department.my-org.com/my-data-offer") .licenseUrl("https://my-department.my-org.com/my-data-offer#license") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(dataOfferData) - )) + .dataSource(dataSource) .build(); providerClient.uiApi().createAsset(asset); diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java index 21d03fbeb..c8c5f453f 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -17,6 +17,7 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; @@ -29,6 +30,8 @@ import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.db.TestDatabase; @@ -413,27 +416,30 @@ private static String generateRandomPayload() { } private String createAssetWithParameterizedMethod(TestCase testCase) { - val proxyProperties = new HashMap<>(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.BASE_URL, sourceUrl - )); + var httpData = new UiDataSourceHttpData(); + httpData.setBaseUrl(sourceUrl); if (testCase.path != null) { - proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyPath", "true"); + httpData.setEnablePathParameterization(true); } if (testCase.body != null) { - proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyBody", "true"); + httpData.setEnableBodyParameterization(true); } if (testCase.method != null) { - proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyMethod", "true"); + httpData.setEnableMethodParameterization(true); } if (testCase.queryParams != null) { - proxyProperties.put("https://w3id.org/edc/v0.0.1/ns/proxyQueryParams", "true"); + httpData.setEnableQueryParameterization(true); } + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(httpData) + .build(); + var asset = UiAssetCreateRequest.builder() .id(testCase.id) .title("My Data Offer") - .dataAddressProperties(proxyProperties) + .dataSource(dataSource) .build(); return providerClient.uiApi().createAsset(asset).getId(); diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java index aad4167c8..3a47e95e6 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java @@ -17,6 +17,7 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; @@ -27,6 +28,8 @@ import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; @@ -42,7 +45,6 @@ import java.util.HashMap; import java.util.List; -import java.util.Map; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; @@ -134,15 +136,18 @@ void testQueryParamsDoubleEncoded() { } private void createAsset() { + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceQueryParamsUrl()) + .queryString(encodedParam) + .build()) + .build(); + var asset = UiAssetCreateRequest.builder() .id(dataOfferId) .title("My Data Offer") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, dataAddress.getDataSourceQueryParamsUrl(), - "https://w3id.org/edc/v0.0.1/ns/queryParams", encodedParam - )) + .dataSource(dataSource) .build(); providerClient.uiApi().createAsset(asset); diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index c4397ec2f..b3e883fc1 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -17,13 +17,14 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; -import de.sovity.edc.client.gen.model.UiAssetEditMetadataRequest; +import de.sovity.edc.client.gen.model.UiAssetEditRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; import de.sovity.edc.client.gen.model.UiContractOffer; import de.sovity.edc.client.gen.model.UiCriterion; @@ -31,6 +32,8 @@ import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; import de.sovity.edc.client.gen.model.UiDataOffer; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.client.gen.model.UiPolicyLiteral; @@ -138,6 +141,13 @@ void provide_consume_assetMapping_policyMapping_agreements() { .build()) .build()).getId(); + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(data)) + .build()) + .build(); + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() .id("asset-1") .title("AssetName") @@ -163,11 +173,7 @@ void provide_consume_assetMapping_policyMapping_agreements() { .temporalCoverageToInclusive(LocalDate.parse("2024-01-22")) .keywords(List.of("keyword1", "keyword2")) .publisherHomepage("publisherHomepage") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) - )) + .dataSource(dataSource) .customJsonAsString(""" {"test": "value"} """) @@ -329,14 +335,17 @@ void provide_consume_assetMapping_policyMapping_agreements() { @Test void canOverrideTheWellKnowPropertiesUsingTheCustomProperties() { // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com/base") + .build()) + .build(); + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() .id("asset-1") .title("will be overridden") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, "http://example.com/base" - )) + .dataSource(dataSource) .customJsonLdAsString(""" { "http://purl.org/dc/terms/title": "The real title", @@ -378,13 +387,16 @@ void customTransferRequest() { // arrange var data = "expected data 123"; + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(data)) + .build()) + .build(); + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() .id("asset-1") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) - )) + .dataSource(dataSource) .build()).getId(); assertThat(assetId).isEqualTo("asset-1"); @@ -437,14 +449,17 @@ void editAssetMetadataOnLiveContract() { // arrange var data = "expected data 123"; + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(data)) + .build()) + .build(); + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() .id("asset-1") .title("Bad Asset Title") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) - )) + .dataSource(dataSource) .customJsonAsString(""" { "test": "value" @@ -493,7 +508,7 @@ void editAssetMetadataOnLiveContract() { var negotiation = negotiate(dataOffer, contractOffer); // act - providerClient.uiApi().editAssetMetadata(assetId, UiAssetEditMetadataRequest.builder() + providerClient.uiApi().editAssetMetadata(assetId, UiAssetEditRequest.builder() .title("Good Asset Title") .customJsonAsString(""" { diff --git a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java index e6df6d88a..a32f56217 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java @@ -20,6 +20,7 @@ import de.sovity.edc.client.gen.model.CatalogFilterExpressionOperator; import de.sovity.edc.client.gen.model.CatalogQuery; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; @@ -27,6 +28,8 @@ import de.sovity.edc.client.gen.model.UiCriterionLiteral; import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.client.gen.model.UiPolicyLiteral; @@ -44,7 +47,6 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Map; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; @@ -137,20 +139,23 @@ private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperat } private void createAsset() { + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(dataOfferData)) + .build()) + .build(); + var asset = UiAssetCreateRequest.builder() - .id(dataOfferId) - .title("My Data Offer") - .description("Example Data Offer.") - .version("2023-11") - .language("EN") - .publisherHomepage("https://my-department.my-org.com/my-data-offer") - .licenseUrl("https://my-department.my-org.com/my-data-offer#license") - .dataAddressProperties(Map.of( - Prop.Edc.TYPE, "HttpData", - Prop.Edc.METHOD, "GET", - Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(dataOfferData) - )) - .build(); + .id(dataOfferId) + .title("My Data Offer") + .description("Example Data Offer.") + .version("2023-11") + .language("EN") + .publisherHomepage("https://my-department.my-org.com/my-data-offer") + .licenseUrl("https://my-department.my-org.com/my-data-offer#license") + .dataSource(dataSource) + .build(); providerClient.uiApi().createAsset(asset); } diff --git a/utils/catalog-parser/README.md b/utils/catalog-parser/README.md index 67b1dc5f6..db7abce22 100644 --- a/utils/catalog-parser/README.md +++ b/utils/catalog-parser/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Utilities:
      Catalog Fetcher / Catalog Parser

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/utils/jooq-database-access/README.md b/utils/jooq-database-access/README.md index 449d06242..0176f4a4e 100644 --- a/utils/jooq-database-access/README.md +++ b/utils/jooq-database-access/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Utilities:
      jOOQ direct database access utilities

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/utils/jooq-database-access/build.gradle.kts b/utils/jooq-database-access/build.gradle.kts index 9ce518294..9a446e5a7 100644 --- a/utils/jooq-database-access/build.gradle.kts +++ b/utils/jooq-database-access/build.gradle.kts @@ -1,10 +1,10 @@ +import nu.studer.gradle.jooq.JooqGenerate import org.flywaydb.gradle.task.FlywayCleanTask import org.flywaydb.gradle.task.FlywayMigrateTask -import org.testcontainers.containers.JdbcDatabaseContainer -import org.testcontainers.containers.PostgreSQLContainer -import nu.studer.gradle.jooq.JooqGenerate import org.jooq.meta.jaxb.ForcedType import org.jooq.meta.jaxb.Nullability +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.containers.PostgreSQLContainer plugins { `java-library` diff --git a/utils/json-and-jsonld-utils/README.md b/utils/json-and-jsonld-utils/README.md index 66dacbbba..8417ae3ce 100644 --- a/utils/json-and-jsonld-utils/README.md +++ b/utils/json-and-jsonld-utils/README.md @@ -1,16 +1,16 @@
      - + Logo

      EDC-Connector Utilities:
      JSON / JSON-LD Utils

      - Report Bug + Report Bug · - Request Feature + Request Feature

      diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java index 7ac01a187..1a91ae7cf 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java @@ -16,11 +16,13 @@ import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonValue; +import jakarta.json.stream.JsonGenerator; import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.io.StringReader; import java.io.StringWriter; +import java.util.Map; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class JsonUtils { @@ -49,4 +51,18 @@ public static String toJson(JsonValue json) { } } + public static String toJsonPretty(JsonValue json) { + if (json == null) { + return "null"; + } + + var config = Map.of(JsonGenerator.PRETTY_PRINTING, true); + + var sw = new StringWriter(); + try (var writer = Json.createWriterFactory(config).createWriter(sw)) { + writer.write(json); + return sw.toString(); + } + } + } diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index 3d35377a0..c9be757ca 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -15,6 +15,13 @@ import lombok.experimental.UtilityClass; +/** + * Constants for used JSON-LD Vocabulary. + *

      + * Please note, that due to how JSON-LD / ontologies are defined, all property names of a namespace are just + * mixed together on the same level. A property, e.g. type, might be used in multiple classes, which is an + * abstraction leak by design. + */ @UtilityClass public class Prop { public final String ID = "@id"; @@ -36,8 +43,12 @@ public class Edc { public final String PRIVATE_PROPERTIES = CTX + "privateProperties"; public final String DATA_ADDRESS = CTX + "dataAddress"; public final String TYPE = CTX + "type"; + public final String DATA_ADDRESS_TYPE_HTTP_DATA = "HttpData"; + public final String DATA_ADDRESS_TYPE_HTTP_PROXY = "HttpData"; public final String BASE_URL = CTX + "baseUrl"; public final String METHOD = CTX + "method"; + public final String CONTENT_TYPE = CTX + "contentType"; + public final String QUERY_PARAMS = CTX + "queryParams"; public final String PROXY_METHOD = CTX + "proxyMethod"; public final String PROXY_PATH = CTX + "proxyPath"; public final String PROXY_QUERY_PARAMS = CTX + "proxyQueryParams"; @@ -126,6 +137,10 @@ public class SovityDcatExt { public final String CTX = "https://semantic.sovity.io/dcat-ext#"; public final String CUSTOM_JSON = CTX + "customJson"; public final String PRIVATE_CUSTOM_JSON = CTX + "privateCustomJson"; + public final String DATA_SOURCE_AVAILABILITY = CTX + "dataSourceAvailability"; + public final String DATA_SOURCE_AVAILABILITY_ON_REQUEST = "ON_REQUEST"; + public final String CONTACT_EMAIL = CTX + "contactEmail"; + public final String CONTACT_PREFERRED_EMAIL_SUBJECT = CTX + "contactPreferredEmailSubject"; @UtilityClass public class HttpDatasourceHints { diff --git a/utils/test-connector-remote/README.md b/utils/test-connector-remote/README.md index d830c70ce..903aa7a09 100644 --- a/utils/test-connector-remote/README.md +++ b/utils/test-connector-remote/README.md @@ -1,16 +1,16 @@

      - + Logo

      Connector Remote (for Test Data)

      - Report Bug + Report Bug · - Request Feature + Request Feature

      From b5af6c7701680a142ceabbb5bbddb504db61d722 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jun 2024 16:13:11 +0200 Subject: [PATCH 253/295] chore(deps-dev): bump braces (#984) --- .../package-lock.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/extensions/wrapper/clients/typescript-client-example/package-lock.json b/extensions/wrapper/clients/typescript-client-example/package-lock.json index 02b8715d5..03d2167b2 100644 --- a/extensions/wrapper/clients/typescript-client-example/package-lock.json +++ b/extensions/wrapper/clients/typescript-client-example/package-lock.json @@ -683,12 +683,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -981,9 +981,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2643,12 +2643,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -2849,9 +2849,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" From 12e6350484432b7e5b7da628aa85d9c8e33787f2 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Mon, 1 Jul 2024 14:09:25 +0200 Subject: [PATCH 254/295] fix: missing authentication header handling in reworked UiDataSource, missing changelog entry (#986) --- CHANGELOG.md | 2 + docs/api/sovity-edc-api-wrapper.yaml | 62 +++++++++++------ .../edc/ext/wrapper/api/ui/UiResource.java | 6 +- .../wrapper/api/common/model/SecretValue.java | 46 +++++++++++++ .../common/model/UiDataSourceHttpData.java | 13 ++++ .../http/HttpDataSourceMapper.java | 11 +++ .../mappers/asset/AssetJsonLdBuilderTest.java | 67 +++++++++++++++++++ .../ext/wrapper/api/ui/UiResourceImpl.java | 2 +- .../ui/pages/asset/AssetApiServiceTest.java | 4 +- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 4 +- .../sovity/edc/utils/jsonld/vocab/Prop.java | 5 +- 11 files changed, 192 insertions(+), 30 deletions(-) create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/SecretValue.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c5ecee2..4507377d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Major Changes +- API Wrapper UI API: Moved to well-typed data sources, breaking changes to the asset model and API. + #### Minor Changes - Add the SovityMessenger extension diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index fd30bbb14..5cd19ce0a 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -136,17 +136,22 @@ paths: schema: $ref: '#/components/schemas/IdResponseDto' /wrapper/ui/pages/asset-page/assets/{assetId}: - delete: + put: tags: - UI - description: Delete an Asset - operationId: deleteAsset + description: Updates an Asset's metadata and optionally also the data source. + operationId: editAsset parameters: - name: assetId in: path required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UiAssetEditRequest' responses: default: description: default response @@ -154,14 +159,13 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' - /wrapper/ui/pages/contract-definition-page/contract-definitions/{contractDefinitionId}: delete: tags: - UI - description: Delete a Contract Definition - operationId: deleteContractDefinition + description: Delete an Asset + operationId: deleteAsset parameters: - - name: contractDefinitionId + - name: assetId in: path required: true schema: @@ -173,14 +177,14 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' - /wrapper/ui/pages/policy-page/policy-definitions/{policyId}: + /wrapper/ui/pages/contract-definition-page/contract-definitions/{contractDefinitionId}: delete: tags: - UI - description: Delete a Policy Definition - operationId: deletePolicyDefinition + description: Delete a Contract Definition + operationId: deleteContractDefinition parameters: - - name: policyId + - name: contractDefinitionId in: path required: true schema: @@ -192,23 +196,18 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' - /wrapper/ui/pages/asset-page/assets/{assetId}/metadata: - put: + /wrapper/ui/pages/policy-page/policy-definitions/{policyId}: + delete: tags: - UI - description: Updates an Asset's metadata - operationId: editAssetMetadata + description: Delete a Policy Definition + operationId: deletePolicyDefinition parameters: - - name: assetId + - name: policyId in: path required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UiAssetEditRequest' responses: default: description: default response @@ -500,6 +499,20 @@ components: - HTTP_DATA - ON_REQUEST - CUSTOM + SecretValue: + type: object + properties: + secretName: + type: string + description: Secret Name / Vault Key Name. + example: myApiAuthKey + rawValue: + type: string + description: Raw inline Value. + example: + description: A value either inlined or to be fetched from the Vault. Raw secret + values will land in the database and will be retrievable via the Eclipse EDC + Management API. UiAssetCreateRequest: required: - dataSource @@ -655,6 +668,13 @@ components: type: string description: HTTP Request Query Params / Query String. example: search=example&limit=10 + authHeaderName: + type: string + description: Auth Header name. The auth header is handled specially by the + EDC as its value can be read from a vault. + example: Authorization + authHeaderValue: + $ref: '#/components/schemas/SecretValue' headers: type: object additionalProperties: diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 90904ca41..abf602c80 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -70,11 +70,11 @@ interface UiResource { IdResponseDto createAsset(UiAssetCreateRequest uiAssetCreateRequest); @PUT - @Path("pages/asset-page/assets/{assetId}/metadata") + @Path("pages/asset-page/assets/{assetId}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @Operation(description = "Updates an Asset's metadata") - IdResponseDto editAssetMetadata(@PathParam("assetId") String assetId, UiAssetEditRequest uiAssetEditRequest); + @Operation(description = "Updates an Asset's metadata and optionally also the data source.") + IdResponseDto editAsset(@PathParam("assetId") String assetId, UiAssetEditRequest uiAssetEditRequest); @DELETE @Path("pages/asset-page/assets/{assetId}") diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/SecretValue.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/SecretValue.java new file mode 100644 index 000000000..519f6f914 --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/SecretValue.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "A value either inlined or to be fetched from the Vault. " + + "Raw secret values will land in the database and will be retrievable via the " + + "Eclipse EDC Management API.") +public class SecretValue { + @Schema( + description = "Secret Name / Vault Key Name.", + example = "myApiAuthKey", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private String secretName; + + @Schema( + description = "Raw inline Value.", + example = "", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private String rawValue; +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java index a44d9935e..381dcdb73 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java @@ -50,6 +50,19 @@ public class UiDataSourceHttpData { ) private String queryString; + @Schema( + description = "Auth Header name. The auth header is handled specially by the EDC as its value can be read from a vault.", + example = "Authorization", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private String authHeaderName; + + @Schema( + description = "Auth Header value.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + private SecretValue authHeaderValue; + @Schema( description = "HTTP Request Headers. HTTP Header Parameterization is not available.", requiredMode = Schema.RequiredMode.NOT_REQUIRED diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java index ffb619fee..32812934d 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java @@ -55,6 +55,17 @@ public Map buildDataAddress(@NonNull UiDataSourceHttpData httpDa props.put(Prop.Edc.QUERY_PARAMS, httpData.getQueryString()); } + if (StringUtils.isNotBlank(httpData.getAuthHeaderName())) { + props.put(Prop.Edc.AUTH_KEY, httpData.getAuthHeaderName()); + if (httpData.getAuthHeaderValue() != null) { + if (httpData.getAuthHeaderValue().getRawValue() != null) { + props.put(Prop.Edc.AUTH_CODE, httpData.getAuthHeaderValue().getRawValue()); + } else if (httpData.getAuthHeaderValue().getSecretName() != null) { + props.put(Prop.Edc.SECRET_NAME, httpData.getAuthHeaderValue().getSecretName()); + } + } + } + props.putAll(httpHeaderMapper.buildHeaderProps(httpData.getHeaders())); // Parameterization diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java index 0b01c947f..ea0b8658e 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java @@ -16,6 +16,7 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.Factory; import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; +import de.sovity.edc.ext.wrapper.api.common.model.SecretValue; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; @@ -448,6 +449,72 @@ void test_create_httpData_bodyParameterization() { assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); } + @Test + void test_create_httpData_authHeader_secretName() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .authHeaderName("X-Test") + .authHeaderValue(SecretValue.builder().secretName("mySecretName").build()) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.AUTH_KEY, "X-Test") + .add(Prop.Edc.SECRET_NAME, "mySecretName"); + + var expectedProperties = dummyAssetCommonProperties(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + + @Test + void test_create_httpData_authHeader_rawValue() { + // arrange + var dataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("https://example.com") + .authHeaderName("X-Test") + .authHeaderValue(SecretValue.builder().rawValue("myKey").build()) + .build()) + .build(); + + var uiAssetCreateRequest = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(ASSET_ID) + .build(); + + var dataAddress = Json.createObjectBuilder() + .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) + .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) + .add(Prop.Edc.BASE_URL, "https://example.com") + .add(Prop.Edc.AUTH_KEY, "X-Test") + .add(Prop.Edc.AUTH_CODE, "myKey"); + + var expectedProperties = dummyAssetCommonProperties(); + + // act + var actual = assetJsonLdBuilder.createAssetJsonLd(uiAssetCreateRequest, ORG_NAME); + + // assert + assertEqualJson(actual, dummyAssetJsonLd(dataAddress, expectedProperties)); + } + @Test void test_create_onRequest() { // arrange diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 4aba52060..983a42f0f 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -76,7 +76,7 @@ public IdResponseDto createAsset(UiAssetCreateRequest uiAssetCreateRequest) { } @Override - public IdResponseDto editAssetMetadata(String assetId, UiAssetEditRequest uiAssetEditRequest) { + public IdResponseDto editAsset(String assetId, UiAssetEditRequest uiAssetEditRequest) { return assetApiService.editAsset(assetId, uiAssetEditRequest); } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java index 0baf08c63..08922d047 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java @@ -226,7 +226,7 @@ void testAssetCreation(AssetService assetService) { } @Test - void testEditAssetMetadata(AssetService assetService) { + void testeditAsset(AssetService assetService) { // arrange var dataSource = UiDataSource.builder() .type(DataSourceType.HTTP_DATA) @@ -318,7 +318,7 @@ void testEditAssetMetadata(AssetService assetService) { .build(); // act - var response = client.uiApi().editAssetMetadata("asset-1", editRequest); + var response = client.uiApi().editAsset("asset-1", editRequest); // assert assertThat(response.getId()).isEqualTo("asset-1"); diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index b3e883fc1..fe0ed5307 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -445,7 +445,7 @@ void customTransferRequest() { @DisabledOnGithub @Test - void editAssetMetadataOnLiveContract() { + void editAssetOnLiveContract() { // arrange var data = "expected data 123"; @@ -508,7 +508,7 @@ void editAssetMetadataOnLiveContract() { var negotiation = negotiate(dataOffer, contractOffer); // act - providerClient.uiApi().editAssetMetadata(assetId, UiAssetEditRequest.builder() + providerClient.uiApi().editAsset(assetId, UiAssetEditRequest.builder() .title("Good Asset Title") .customJsonAsString(""" { diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index c9be757ca..ea04f5e43 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -44,11 +44,14 @@ public class Edc { public final String DATA_ADDRESS = CTX + "dataAddress"; public final String TYPE = CTX + "type"; public final String DATA_ADDRESS_TYPE_HTTP_DATA = "HttpData"; - public final String DATA_ADDRESS_TYPE_HTTP_PROXY = "HttpData"; + public final String DATA_ADDRESS_TYPE_HTTP_PROXY = "HttpProxy"; public final String BASE_URL = CTX + "baseUrl"; public final String METHOD = CTX + "method"; public final String CONTENT_TYPE = CTX + "contentType"; public final String QUERY_PARAMS = CTX + "queryParams"; + public final String AUTH_KEY = CTX + "authKey"; + public final String AUTH_CODE = CTX + "authCode"; + public final String SECRET_NAME = CTX + "secretName"; public final String PROXY_METHOD = CTX + "proxyMethod"; public final String PROXY_PATH = CTX + "proxyPath"; public final String PROXY_QUERY_PARAMS = CTX + "proxyQueryParams"; From 7bf5f3e01867c3d38ecaf114ecd5382d4cbb5dc7 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:22:58 +0200 Subject: [PATCH 255/295] ci: add workflow for automated mds-issue-referencing --- .github/workflows/mds_issues_referencing.yml | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/mds_issues_referencing.yml diff --git a/.github/workflows/mds_issues_referencing.yml b/.github/workflows/mds_issues_referencing.yml new file mode 100644 index 000000000..d31a11f9a --- /dev/null +++ b/.github/workflows/mds_issues_referencing.yml @@ -0,0 +1,47 @@ +name: Reference MDS issues ind MDS Org-Repo + +on: + issues: + types: [opened] + +jobs: + mds-issues-referencing: + if: contains(github.event.issue.labels.*.name, 'kind/bug') + && ( + contains('AdamRaven', github.event.issue.user.login) || + contains('alexanderaukam', github.event.issue.user.login) || + contains('cristianivanescutsystems', github.event.issue.user.login) || + contains('DanielHeiderMDS', github.event.issue.user.login) || + contains('dhommen', github.event.issue.user.login) || + contains('DianaMDS', github.event.issue.user.login) || + contains('drmVR', github.event.issue.user.login) || + contains('ip312', github.event.issue.user.login) || + contains('juliusmeyer', github.event.issue.user.login) || + contains('MaichiNguyenMDS', github.event.issue.user.login) || + contains('maxschmidMDS', github.event.issue.user.login) || + contains('MoritzStober-acatech', github.event.issue.user.login) || + contains('nb-mds', github.event.issue.user.login) || + contains('robinidento', github.event.issue.user.login) || + contains('schemetzko', github.event.issue.user.login) || + contains('schoenenberg', github.event.issue.user.login) || + contains('sebplorenz', github.event.issue.user.login) || + contains('StraeterRainer', github.event.issue.user.login) + ) + runs-on: ubuntu-latest + + steps: + - name: Create issue in MDS Org-Repo + env: + MDS_ISSUE_CREATOR: ${{ secrets.MDS_ISSUE_CREATOR }} #PAT of the account who has permission in the MDS repo and will also be the creator of the issues as secret + MDS_ORG_NAME: ${{ secrets.MDS_ORG_NAME }} #The MDS GitHub Org name as secret + MDS_REPO_NAME: ${{ secrets.MDS_REPO_NAME }} #The MDS target repo name as secret + run: | + ISSUE_TITLE="${{ github.event.issue.title }}" + ISSUE_URL="${{ github.event.issue.html_url }}" + MDS_BODY="Automatically created issue as referece for: [${ISSUE_URL}](${ISSUE_URL})" + JSON_PAYLOAD=$(jq -n --arg title "$ISSUE_TITLE" --arg body "$MDS_BODY" '{title: $title, body: $body}') + curl -X POST \ + -H "Authorization: token $MDS_ISSUE_CREATOR" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${MDS_ORG_NAME}/${MDS_REPO_NAME}/issues \ + -d "$JSON_PAYLOAD" From 33726cc6a83f325f930a1ff028663721d506befa Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 3 Jul 2024 10:12:02 +0200 Subject: [PATCH 256/295] feat: contract termination API (#987) --- docs/api/sovity-edc-api-wrapper.yaml | 92 ++++++++++++++++++- .../edc/ext/wrapper/api/ui/UiResource.java | 18 +++- .../api/ui/model/ContractAgreementCard.java | 6 ++ .../ui/model/ContractAgreementPageQuery.java | 30 ++++++ .../ContractAgreementTerminationInfo.java | 43 +++++++++ .../ui/model/ContractTerminationRequest.java | 42 +++++++++ .../ui/model/ContractTerminationStatus.java | 19 ++++ .../ext/wrapper/api/ui/UiResourceImpl.java | 11 ++- .../ContractAgreementPageCardBuilder.java | 4 + .../ContractAgreementPageTest.java | 2 +- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 6 +- 11 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 5cd19ce0a..8c5292975 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -249,11 +249,16 @@ paths: items: $ref: '#/components/schemas/UiDataOffer' /wrapper/ui/pages/contract-agreement-page: - get: + post: tags: - UI - description: Collect all data for the Contract Agreement Page + description: Collect filtered data for the Contract Agreement Page operationId: getContractAgreementPage + requestBody: + content: + '*/*': + schema: + $ref: '#/components/schemas/ContractAgreementPageQuery' responses: default: description: default response @@ -407,6 +412,31 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/content-agreement-page/{contractAgreementId}/terminate: + post: + tags: + - UI + description: Terminates a contract agreement designated by its contract agreement + id. + operationId: terminateContractAgreement + parameters: + - name: contractAgreementId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContractTerminationRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' /wrapper/use-case-api/policy-definition: post: tags: @@ -494,11 +524,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource - default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM + default: CUSTOM SecretValue: type: object properties: @@ -706,7 +736,6 @@ components: UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource - default: GET enum: - GET - POST @@ -714,6 +743,7 @@ components: - PATCH - DELETE - OPTIONS + default: GET UiDataSourceOnRequest: required: - contactEmail @@ -1261,6 +1291,7 @@ components: - counterPartyAddress - counterPartyId - direction + - terminationStatus - transferProcesses type: object properties: @@ -1291,6 +1322,14 @@ components: description: Contract Agreement's Transfer Processes items: $ref: '#/components/schemas/ContractAgreementTransferProcess' + terminationStatus: + type: string + description: Contract Agreement's Termination Status + enum: + - ONGOING + - TERMINATED + terminationInformation: + $ref: '#/components/schemas/ContractAgreementTerminationInfo' description: Contract Agreement for Contract Agreement Page ContractAgreementDirection: type: string @@ -1309,6 +1348,26 @@ components: items: $ref: '#/components/schemas/ContractAgreementCard' description: Data as required by the UI's Contract Agreement Page + ContractAgreementTerminationInfo: + required: + - detail + - reason + - terminatedAt + type: object + properties: + terminatedAt: + type: string + description: Termination's date and time + format: date-time + reason: + title: Termination's reason + type: string + description: The termination's nature e.g. User Termination + detail: + type: string + description: Detailed message from the terminating party about why the contract + was terminated. + description: Contract's agreement metadata ContractAgreementTransferProcess: required: - lastUpdatedDate @@ -1354,6 +1413,17 @@ components: simplifiedState: $ref: '#/components/schemas/TransferProcessSimplifiedState' description: Transfer Process State interpreted + ContractAgreementPageQuery: + type: object + properties: + status: + type: string + description: Optionally filter the resulting contract agreements by their + termination status. + enum: + - ONGOING + - TERMINATED + description: Filters for querying a Contract Contract Agreement Page ContractDefinitionEntry: required: - accessPolicyId @@ -1706,6 +1776,20 @@ components: description: Additional transfer process properties. These are not passed to the consumer EDC description: "For type PARAMS_ONLY: Required data for starting a Transfer Process" + ContractTerminationRequest: + required: + - reason + type: object + properties: + reason: + title: Termination reason + type: string + description: A short reason why this contract was terminated + detail: + title: Termination detail + type: string + description: A user explanation to detail why the contract was terminated. + description: Data for terminating a Contract Agreement AtomicConstraintDto: required: - leftExpression diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index abf602c80..aa12367c2 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -20,9 +20,11 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPageQuery; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; @@ -43,6 +45,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -139,11 +142,11 @@ interface UiResource { @Operation(description = "Get Contract Negotiation Information") UiContractNegotiation getContractNegotiation(@PathParam("contractNegotiationId") String contractNegotiationId); - @GET + @POST @Path("pages/contract-agreement-page") @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Collect all data for the Contract Agreement Page") - ContractAgreementPage getContractAgreementPage(); + @Operation(description = "Collect filtered data for the Contract Agreement Page") + ContractAgreementPage getContractAgreementPage(@Nullable ContractAgreementPageQuery contractAgreementPageQuery); @POST @Path("pages/contract-agreement-page/transfers") @@ -159,6 +162,15 @@ interface UiResource { @Operation(description = "Initiate a Transfer Process via a custom Transfer Process JSON-LD. Fields such as connectorId, assetId, providerConnectorId, providerConnectorAddress will be set automatically.") IdResponseDto initiateCustomTransfer(InitiateCustomTransferRequest initiateCustomTransferRequest); + @POST + @Path("pages/content-agreement-page/{contractAgreementId}/terminate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Terminates a contract agreement designated by its contract agreement id.") + IdResponseDto terminateContractAgreement( + @PathParam("contractAgreementId") String contractAgreementId, + ContractTerminationRequest contractTerminationRequest); + @GET @Path("pages/transfer-history-page") @Produces(MediaType.APPLICATION_JSON) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java index d2f8eee7c..0f9568f0a 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementCard.java @@ -59,4 +59,10 @@ public class ContractAgreementCard { @Schema(description = "Contract Agreement's Transfer Processes", requiredMode = Schema.RequiredMode.REQUIRED) private List transferProcesses; + + @Schema(description = "Contract Agreement's Termination Status", requiredMode = Schema.RequiredMode.REQUIRED) + private ContractTerminationStatus terminationStatus; + + @Schema(description = "Contract Agreement's Metadata", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private ContractAgreementTerminationInfo terminationInformation; } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java new file mode 100644 index 000000000..c4803a142 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Filters for querying a Contract Contract Agreement Page") +public class ContractAgreementPageQuery { + @Schema( + description = "Optionally filter the resulting contract agreements by their termination status.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private ContractTerminationStatus terminationStatus; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java new file mode 100644 index 000000000..d96223ef7 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Contract's agreement metadata") +public class ContractAgreementTerminationInfo { + + @Schema(description = "Termination's date and time", requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime terminatedAt; + + @Schema( + title = "Termination's reason", + description = "The termination's nature e.g. User Termination", + requiredMode = Schema.RequiredMode.REQUIRED) + private String reason; + + @Schema( + description = "Detailed message from the terminating party about why the contract was terminated.", + requiredMode = Schema.RequiredMode.REQUIRED + ) + private String detail; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java new file mode 100644 index 000000000..45a98dfcd --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@RequiredArgsConstructor +@Schema(description = "Data for terminating a Contract Agreement") +public class ContractTerminationRequest { + + @Schema( + title = "Termination reason", + description = "A short reason why this contract was terminated", + requiredMode = Schema.RequiredMode.REQUIRED) + String reason; + + @Schema( + title = "Termination detail", + description = "A user explanation to detail why the contract was terminated.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String detail; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java new file mode 100644 index 000000000..b802b60ba --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +public enum ContractTerminationStatus { + ONGOING, + TERMINATED +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 983a42f0f..872c4a651 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -20,6 +20,8 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPageQuery; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; @@ -42,6 +44,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -131,10 +134,11 @@ public UiContractNegotiation getContractNegotiation(String contractNegotiationId } @Override - public ContractAgreementPage getContractAgreementPage() { + public ContractAgreementPage getContractAgreementPage(@Nullable ContractAgreementPageQuery contractAgreementPageQuery) { return contractAgreementApiService.contractAgreementPage(); } + @Override public IdResponseDto initiateTransfer(InitiateTransferRequest request) { return contractAgreementTransferApiService.initiateTransfer(request); @@ -145,6 +149,11 @@ public IdResponseDto initiateCustomTransfer(InitiateCustomTransferRequest reques return contractAgreementTransferApiService.initiateCustomTransfer(request); } + @Override + public IdResponseDto terminateContractAgreement(String contractAgreementId, ContractTerminationRequest contractTerminationRequest) { + return null; + } + @Override public TransferHistoryPage getTransferHistoryPage() { return new TransferHistoryPage(transferHistoryPageApiService.getTransferHistoryEntries()); diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java index d090c4401..f7a7c2644 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java @@ -19,6 +19,8 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementDirection; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTransferProcess; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTerminationInfo; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationStatus; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -61,6 +63,8 @@ public ContractAgreementCard buildContractAgreementCard( card.setAsset(assetMapper.buildUiAsset(asset, assetConnectorEndpoint, assetParticipantId)); card.setContractPolicy(policyMapper.buildUiPolicy(agreement.getPolicy())); card.setTransferProcesses(buildTransferProcesses(transferProcesses)); + card.setTerminationStatus(ContractTerminationStatus.ONGOING); + card.setTerminationInformation(null); return card; } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java index b8defda15..8aca8a06f 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java @@ -85,7 +85,7 @@ void testContractAgreementPage( transferProcessStore.save(transferProcess(1, 1, TransferProcessStates.COMPLETED.code())); // act - var actual = client.uiApi().getContractAgreementPage().getContractAgreements(); + var actual = client.uiApi().getContractAgreementPage(null).getContractAgreements(); assertThat(actual).hasSize(1); // assert diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index fe0ed5307..5cad0820f 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -216,8 +216,8 @@ void provide_consume_assetMapping_policyMapping_agreements() { // act var negotiation = negotiate(dataOffer, contractOffer); initiateTransfer(negotiation); - var providerAgreements = providerClient.uiApi().getContractAgreementPage().getContractAgreements(); - var consumerAgreements = consumerClient.uiApi().getContractAgreementPage().getContractAgreements(); + var providerAgreements = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements(); + var consumerAgreements = consumerClient.uiApi().getContractAgreementPage(null).getContractAgreements(); // assert assertThat(dataOffer.getEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); @@ -541,7 +541,7 @@ void editAssetOnLiveContract() { // assert assertThat(consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); - val firstAsset = providerClient.uiApi().getContractAgreementPage().getContractAgreements().get(0).getAsset(); + val firstAsset = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements().get(0).getAsset(); assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" { From e7fc1080c26d0225d7d0dd865324fde2812178fe Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:25:27 +0200 Subject: [PATCH 257/295] feat: authority portal catalog crawler (#985) Co-authored-by: Richard Treier --- .dockerignore | 2 +- .editorconfig | 11 + .env | 1 - .env.dev | 1 - .github/ISSUE_TEMPLATE/release.md | 7 +- .../actions/build-connector-image/action.yml | 2 +- .github/markdown-link-checker-config.jq | 1 - .github/workflows/ci.yml | 27 +- .github/workflows/license_scan.yml | 2 - .gitignore | 3 +- CHANGELOG.md | 33 +- archived/broker/README.md | 246 -- build.gradle.kts | 6 + connector/README.md | 39 - docker-compose-dev.yaml | 66 - docker-compose.yaml | 66 - docs/api/sovity-edc-api-wrapper.yaml | 6 +- .../goals/broker-production/README.md | 130 - .../catalog-crawler-production/README.md | 55 + .../goals/local-demo/README.md | 18 +- .../goals/production/4.2.0/README.md | 3 +- docs/dev/checkstyle/checkstyle-config.xml | 14 +- extensions/broker-server-api/api/README.md | 27 - .../broker-server-api/api/build.gradle.kts | 79 - .../ext/brokerserver/api/ApiInformation.java | 44 - .../api/BrokerServerResource.java | 111 - .../api/model/AddedConnector.java | 34 - ...horityPortalConnectorDataOfferDetails.java | 38 - ...AuthorityPortalConnectorDataOfferInfo.java | 50 - .../model/AuthorityPortalConnectorInfo.java | 42 - .../AuthorityPortalOrganizationMetadata.java | 36 - ...rityPortalOrganizationMetadataRequest.java | 34 - .../api/model/CatalogContractOffer.java | 46 - .../api/model/CatalogDataOffer.java | 59 - .../api/model/CatalogPageQuery.java | 43 - .../api/model/CatalogPageResult.java | 45 - .../api/model/CatalogPageSortingItem.java | 36 - .../ext/brokerserver/api/model/CnfFilter.java | 38 - .../api/model/CnfFilterAttribute.java | 40 - .../brokerserver/api/model/CnfFilterItem.java | 36 - .../api/model/CnfFilterValue.java | 36 - .../api/model/CnfFilterValueAttribute.java | 38 - .../api/model/ConnectorCreationRequest.java | 34 - .../api/model/ConnectorDetailPageQuery.java | 34 - .../api/model/ConnectorDetailPageResult.java | 59 - .../api/model/ConnectorListEntry.java | 57 - .../api/model/ConnectorOnlineStatus.java | 25 - .../api/model/ConnectorPageQuery.java | 40 - .../api/model/ConnectorPageResult.java | 41 - .../api/model/ConnectorPageSortingItem.java | 36 - .../api/model/ConnectorPageSortingType.java | 31 - .../model/DataOfferDetailContractOffer.java | 46 - .../api/model/DataOfferDetailPageQuery.java | 37 - .../api/model/DataOfferDetailPageResult.java | 61 - .../api/model/PaginationMetadata.java | 43 - .../broker-server-api/client-ts/.gitignore | 24 - .../client-ts/.prettierignore | 2 - .../broker-server-api/client-ts/README.md | 55 - .../broker-server-api/client-ts/index.html | 12 - .../client-ts/package-lock.json | 3182 ----------------- .../broker-server-api/client-ts/package.json | 55 - .../client-ts/prettier.config.cjs | 26 - .../client-ts/src/BrokerServerClient.ts | 42 - .../client-ts/src/generated/.gitignore | 2 - .../broker-server-api/client-ts/src/index.ts | 2 - .../client-ts/src/vite-env.d.ts | 1 - .../broker-server-api/client-ts/tsconfig.json | 19 - .../client-ts/vite.config.ts | 17 - extensions/broker-server-api/client/README.md | 69 - .../broker-server-api/client/build.gradle.kts | 137 - .../client/BrokerServerClient.java | 32 - .../client/BrokerServerClientBuilder.java | 38 - .../client/BrokerServerClientFactory.java | 42 - .../db/DslContextFactoryHijacker.java | 38 - .../ext/brokerserver/db/FlywayFactory.java | 48 - .../ext/brokerserver/db/FlywayMigrator.java | 97 - .../db/PostgresFlywayExtension.java | 65 - .../brokerserver/db/utils/ConfigUtils.java | 30 - .../db/utils/JdbcCredentials.java | 39 - ...rg.eclipse.edc.spi.system.ServiceExtension | 1 - .../main/resources/db/migration/V1__MS8.sql | 529 --- .../main/resources/db/migration/V2__PoC.sql | 94 - .../main/resources/db/migration/V3_1__MvP.sql | 36 - .../migration/V3_2__MvP_Non_Transactional.sql | 10 - .../db/migration/V4_1__MvP_Bugfixes_1_1_0.sql | 9 - .../V4_2__MvP_Bugfixes_Non_Transactional.sql | 5 - .../V5_1__MvP_Fix_Asset_JSON_Properties.sql | 18 - .../main/resources/db/migration/V6__EDC0.sql | 166 - .../migration/V7__Organization_Metadata.sql | 11 - .../db/testdata/V2_1__PoC_Test_Data.sql | 69 - extensions/broker-server/README.md | 38 - .../brokerserver/BrokerServerExtension.java | 126 - .../BrokerServerExtensionContext.java | 49 - .../BrokerServerExtensionContextBuilder.java | 394 -- .../BrokerServerResourceImpl.java | 116 - .../brokerserver/dao/ConnectorQueries.java | 54 - .../CatalogQueryAvailableFilterFetcher.java | 74 - .../CatalogQueryContractOfferFetcher.java | 51 - .../catalog/CatalogQueryDataOfferFetcher.java | 97 - .../dao/pages/catalog/CatalogQueryFields.java | 129 - .../catalog/CatalogQueryFilterService.java | 62 - .../pages/catalog/CatalogQueryService.java | 72 - .../catalog/CatalogQuerySortingService.java | 55 - .../models/AvailableFilterValuesQuery.java | 30 - .../pages/catalog/models/CatalogPageRs.java | 31 - .../catalog/models/CatalogQueryFilter.java | 24 - .../CatalogQuerySelectedFilterQuery.java | 30 - .../catalog/models/DataOfferListEntryRs.java | 41 - .../dao/pages/catalog/models/PageQuery.java | 18 - .../ConnectorDetailQueryService.java | 62 - .../connector/ConnectorListQueryService.java | 83 - .../connector/model/ConnectorDetailsRs.java | 38 - .../connector/model/ConnectorListEntryRs.java | 37 - .../DataOfferDetailPageQueryService.java | 59 - .../dao/pages/dataoffer/ViewCountLogger.java | 27 - .../dataoffer/model/ContractOfferRs.java | 32 - .../dataoffer/model/DataOfferDetailRs.java | 41 - .../dao/utils/JsonDeserializationUtils.java | 39 - .../ext/brokerserver/dao/utils/LikeUtils.java | 57 - .../brokerserver/dao/utils/MultisetUtils.java | 30 - .../brokerserver/dao/utils/SearchUtils.java | 51 - .../services/ConnectorCleaner.java | 30 - .../services/ConnectorCreator.java | 65 - .../services/ConnectorKiller.java | 29 - .../services/KnownConnectorsInitializer.java | 44 - .../services/OfflineConnectorKiller.java | 40 - .../services/api/AssetPropertyParser.java | 35 - ...ityPortalConnectorDataOfferApiService.java | 41 - ...rityPortalConnectorMetadataApiService.java | 40 - .../AuthorityPortalConnectorQueryService.java | 123 - ...yPortalOrganizationMetadataApiService.java | 44 - .../services/api/CatalogApiService.java | 143 - .../services/api/ConnectorApiService.java | 84 - .../api/ConnectorDetailApiService.java | 47 - .../services/api/ConnectorListApiService.java | 78 - .../api/ConnectorOnlineStatusMapper.java | 31 - .../services/api/ConnectorService.java | 57 - .../api/DataOfferDetailApiService.java | 89 - .../services/api/DataOfferMappingUtils.java | 40 - .../services/api/PaginationMetadataUtils.java | 49 - .../api/filtering/AttributeFilterQuery.java | 34 - .../CatalogFilterAttributeDefinition.java | 33 - ...talogFilterAttributeDefinitionService.java | 55 - .../api/filtering/CatalogFilterService.java | 161 - .../api/filtering/CatalogSearchService.java | 41 - .../services/config/AdminApiKeyValidator.java | 30 - .../config/BrokerServerSettingsFactory.java | 98 - .../services/config/DataSpaceConfig.java | 20 - .../services/config/DataSpaceConnector.java | 22 - .../services/logging/BrokerEventLogger.java | 131 - .../logging/BrokerExecutionTimeLogger.java | 40 - .../services/queue/ConnectorQueue.java | 43 - .../ConnectorUpdateSuccessWriter.java | 78 - .../ConnectorUnreachableException.java | 21 - .../offers/DataOfferPatchApplier.java | 50 - .../offers/DataOfferRecordUpdater.java | 206 -- .../refreshing/offers/DataOfferWriter.java | 46 - .../offers/FetchedCatalogBuilder.java | 98 - .../offers/model/DataOfferPatch.java | 65 - .../ext/brokerserver/utils/MdsIdUtils.java | 33 - .../ext/brokerserver/utils/StreamUtils2.java | 39 - .../edc/ext/brokerserver/utils/UrlUtils.java | 35 - ...rg.eclipse.edc.spi.system.ServiceExtension | 1 - .../edc/ext/brokerserver/TestAsset.java | 99 - .../edc/ext/brokerserver/TestPolicy.java | 70 - .../edc/ext/brokerserver/TestUtils.java | 120 - .../brokerserver/dao/utils/LikeUtilsTest.java | 26 - .../ext/brokerserver/db/FlywayTestUtils.java | 42 - .../edc/ext/brokerserver/db/TestDatabase.java | 69 - .../brokerserver/db/TestDatabaseFactory.java | 31 - .../brokerserver/db/TestDatabaseViaEnv.java | 57 - .../db/TestDatabaseViaTestcontainers.java | 47 - .../services/api/AddConnectorsApiTest.java | 163 - ...thorityPortalConnectorMetadataApiTest.java | 149 - .../api/AuthorityPortalDataOfferApiTest.java | 180 - ...rityPortalOrganizationMetadataApiTest.java | 117 - .../services/api/CatalogApiTest.java | 708 ---- .../services/api/ConnectorApiTest.java | 159 - .../services/api/DataOfferDetailApiTest.java | 148 - .../services/api/DeleteConnectorsApiTest.java | 158 - .../logging/BrokerEventLoggerTest.java | 62 - .../services/queue/ThreadPoolQueueTest.java | 47 - .../services/queue/ThreadPoolTest.java | 101 - .../refreshing/ConnectorUpdaterTest.java | 206 -- .../offers/DataOfferWriterTestDydi.java | 41 - .../OfflineConnectorRemovalJobTest.java | 114 - .../ext/brokerserver/utils/UrlUtilsTest.java | 33 - extensions/catalog-crawler/README.md | 39 + .../catalog-crawler-db}/build.gradle.kts | 39 +- .../V9999__Make_Columns_Nullable.sql | 25 + .../migration/V1__Initial_Schema.sql | 50 + .../migration/V2__Schema_Extension.sql | 12 + .../migration/V3__Schema_Extension.sql | 28 + .../migration/V4__Schema_Extension.sql | 14 + .../migration/V5__Schema_Extension.sql | 46 + .../migration/V6__AP_Release_1_0_0.sql | 39 + .../migration/V7__Schema_Extension.sql | 3 + .../migration/V8__Schema_Extension.sql | 4 + .../migration/V9__Broker_Integration.sql | 107 + .../catalog-crawler-e2e-test/build.gradle.kts | 44 + .../ext/catalog/crawler/CrawlerE2eTest.java | 234 ++ .../crawler/utils/CrawlerDbAccess.java | 36 + .../ext/catalog/crawler/utils/TestData.java | 57 + .../src/test/resources/logging.properties | 2 +- .../build.gradle.kts | 19 + .../catalog-crawler}/build.gradle.kts | 41 +- .../ext/catalog/crawler/CrawlerExtension.java | 147 + .../crawler/CrawlerExtensionContext.java | 42 + .../CrawlerExtensionContextBuilder.java | 312 ++ .../catalog/crawler/CrawlerInitializer.java} | 10 +- .../crawler/crawling/ConnectorCrawler.java} | 47 +- .../crawling/OfflineConnectorCleaner.java | 42 + .../fetching/FetchedCatalogBuilder.java | 102 + .../fetching/FetchedCatalogMappingUtils.java | 56 + .../fetching/FetchedCatalogService.java} | 19 +- .../fetching}/model/FetchedCatalog.java | 5 +- .../fetching}/model/FetchedContractOffer.java | 4 +- .../fetching}/model/FetchedDataOffer.java | 15 +- .../logging/ConnectorChangeTracker.java | 2 +- .../logging/CrawlerEventErrorMessage.java} | 18 +- .../crawling/logging/CrawlerEventLogger.java | 127 + .../logging/CrawlerExecutionTimeLogger.java | 43 + .../writing/CatalogPatchBuilder.java} | 56 +- .../writing/ConnectorUpdateCatalogWriter.java | 53 + .../ConnectorUpdateFailureWriter.java | 28 +- .../writing/ConnectorUpdateSuccessWriter.java | 71 + .../writing}/DataOfferLimitsEnforcer.java | 46 +- .../crawling/writing/utils/ChangeTracker.java | 36 + .../crawling/writing/utils}/DiffUtils.java | 32 +- .../catalog/crawler/dao/CatalogCleaner.java | 39 + .../ext/catalog/crawler/dao/CatalogPatch.java | 42 + .../crawler/dao/CatalogPatchApplier.java | 46 + .../crawler/dao/config/DataSourceFactory.java | 63 + .../dao/config}/DslContextFactory.java | 25 +- .../crawler/dao/config/FlywayService.java | 54 + .../dao/connectors/ConnectorQueries.java | 73 + .../crawler/dao/connectors/ConnectorRef.java} | 22 +- .../connectors/ConnectorStatusUpdater.java | 34 + .../ContractOfferQueries.java | 10 +- .../ContractOfferRecordUpdater.java | 48 +- .../dao/data_offers}/DataOfferQueries.java | 10 +- .../data_offers/DataOfferRecordUpdater.java | 157 + .../crawler}/dao/utils/JsonbUtils.java | 2 +- .../crawler}/dao/utils/PostgresqlUtils.java | 4 +- .../crawler/dao/utils/RecordPatch.java | 46 + .../orchestration/config/CrawlerConfig.java} | 12 +- .../config/CrawlerConfigFactory.java | 54 + .../config/EdcConfigPropertyUtils.java | 6 +- .../orchestration/queue/ConnectorQueue.java | 48 + .../queue/ConnectorQueueFiller.java | 10 +- .../queue/ConnectorRefreshPriority.java | 5 +- .../orchestration}/queue/ThreadPool.java | 17 +- .../orchestration}/queue/ThreadPoolTask.java | 5 +- .../queue/ThreadPoolTaskQueue.java | 7 +- .../schedules/DeadConnectorRefreshJob.java | 10 +- .../OfflineConnectorCleanerJob.java} | 12 +- .../schedules/OfflineConnectorRefreshJob.java | 10 +- .../schedules/OnlineConnectorRefreshJob.java | 10 +- .../schedules/QuartzScheduleInitializer.java | 6 +- .../schedules/utils/CronJobRef.java | 10 +- .../schedules/utils/JobFactoryImpl.java | 2 +- .../crawler}/utils/CollectionUtils2.java | 10 +- .../catalog/crawler}/utils/JsonUtils2.java | 8 +- .../ext/catalog/crawler}/utils/MapUtils.java | 2 +- .../catalog/crawler}/utils/StringUtils2.java | 23 +- ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../ext/catalog/crawler}/AssertionUtils.java | 5 +- .../ext/catalog/crawler/CrawlerTestDb.java | 63 + .../ext/catalog/crawler/JsonTestUtils.java | 23 + .../edc/ext/catalog/crawler/TestData.java | 66 + .../logging/CrawlerEventLoggerTest.java | 58 + .../ConnectorUpdateCatalogWriterTest.java} | 53 +- .../writing}/DataOfferLimitsEnforcerTest.java | 56 +- .../DataOfferWriterTestDataHelper.java | 87 +- .../DataOfferWriterTestDataModels.java | 2 +- .../writing/DataOfferWriterTestDydi.java | 48 + .../DataOfferWriterTestResultHelper.java | 12 +- .../crawling/writing}/DiffUtilsTest.java | 6 +- .../dao/connectors/ConnectorRefTest.java | 67 + .../queue/ThreadPoolQueueTest.java | 55 + .../OfflineConnectorRemovalJobTest.java | 101 + .../crawler}/utils/CollectionUtils2Test.java | 18 +- .../crawler}/utils/JsonUtils2Test.java | 2 +- .../crawler}/utils/StringUtils2Test.java | 20 +- .../src/test/resources/logging.properties | 6 + .../README.md | 14 +- .../postgres-flyway-core/build.gradle.kts | 26 + .../postgresql/FlywayExecutionParams.java | 50 + .../edc/extension/postgresql/FlywayUtils.java | 141 + .../postgresql/HikariDataSourceFactory.java} | 31 +- .../postgresql/JdbcCredentials.java} | 16 +- .../postgresql/PostgresFlywayExtension.java | 6 + extensions/wrapper/README.md | 4 +- .../wrapper/wrapper-common-api/README.md | 3 +- .../wrapper/wrapper-common-mappers/README.md | 5 +- .../TransferHistoryPageApiService.java | 5 +- .../PolicyDefinitionApiServiceTest.java | 1 + gradle/libs.versions.toml | 2 +- .../{.env.broker => .env.catalog-crawler} | 60 +- launchers/README.md | 15 +- .../broker-server-ce/build.gradle.kts | 35 - .../broker-server-dev/build.gradle.kts | 35 - .../catalog-crawler-ce/build.gradle.kts | 23 + .../catalog-crawler-dev/build.gradle.kts | 23 + .../connectors/sovity-dev/build.gradle.kts | 1 + settings.gradle.kts | 17 +- .../config/ConnectorConfigFactory.java | 2 +- .../e2e/db/EdcRuntimeExtensionDeferred.java | 36 + .../e2e/db/EdcRuntimeExtensionFixed.java | 184 + .../EdcRuntimeExtensionWithTestDatabase.java | 43 + 310 files changed, 4095 insertions(+), 15226 deletions(-) delete mode 100644 archived/broker/README.md delete mode 100644 connector/README.md delete mode 100644 docs/deployment-guide/goals/broker-production/README.md create mode 100644 docs/deployment-guide/goals/catalog-crawler-production/README.md delete mode 100644 extensions/broker-server-api/api/README.md delete mode 100644 extensions/broker-server-api/api/build.gradle.kts delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java delete mode 100644 extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java delete mode 100644 extensions/broker-server-api/client-ts/.gitignore delete mode 100644 extensions/broker-server-api/client-ts/.prettierignore delete mode 100644 extensions/broker-server-api/client-ts/README.md delete mode 100644 extensions/broker-server-api/client-ts/index.html delete mode 100644 extensions/broker-server-api/client-ts/package-lock.json delete mode 100644 extensions/broker-server-api/client-ts/package.json delete mode 100644 extensions/broker-server-api/client-ts/prettier.config.cjs delete mode 100644 extensions/broker-server-api/client-ts/src/BrokerServerClient.ts delete mode 100644 extensions/broker-server-api/client-ts/src/generated/.gitignore delete mode 100644 extensions/broker-server-api/client-ts/src/index.ts delete mode 100644 extensions/broker-server-api/client-ts/src/vite-env.d.ts delete mode 100644 extensions/broker-server-api/client-ts/tsconfig.json delete mode 100644 extensions/broker-server-api/client-ts/vite.config.ts delete mode 100644 extensions/broker-server-api/client/README.md delete mode 100644 extensions/broker-server-api/client/build.gradle.kts delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java delete mode 100644 extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql delete mode 100644 extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql delete mode 100644 extensions/broker-server/README.md delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java delete mode 100644 extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java delete mode 100644 extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java delete mode 100644 extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java create mode 100644 extensions/catalog-crawler/README.md rename extensions/{broker-server-postgres-flyway-jooq => catalog-crawler/catalog-crawler-db}/build.gradle.kts (78%) create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration-test-utils/V9999__Make_Columns_Nullable.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V1__Initial_Schema.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V2__Schema_Extension.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V3__Schema_Extension.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V4__Schema_Extension.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V5__Schema_Extension.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V6__AP_Release_1_0_0.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V7__Schema_Extension.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V8__Schema_Extension.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql create mode 100644 extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts create mode 100644 extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java create mode 100644 extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CrawlerDbAccess.java create mode 100644 extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java rename extensions/{broker-server => catalog-crawler/catalog-crawler-e2e-test}/src/test/resources/logging.properties (88%) create mode 100644 extensions/catalog-crawler/catalog-crawler-launcher-base/build.gradle.kts rename extensions/{broker-server => catalog-crawler/catalog-crawler}/build.gradle.kts (55%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContext.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerInitializer.java} (56%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/ConnectorCrawler.java} (53%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/OfflineConnectorCleaner.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogBuilder.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogMappingUtils.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogService.java} (59%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching}/model/FetchedCatalog.java (80%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching}/model/FetchedContractOffer.java (86%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching}/model/FetchedDataOffer.java (68%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling}/logging/ConnectorChangeTracker.java (96%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventErrorMessage.java} (57%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLogger.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerExecutionTimeLogger.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/CatalogPatchBuilder.java} (67%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriter.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/ConnectorUpdateFailureWriter.java (51%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateSuccessWriter.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/DataOfferLimitsEnforcer.java (62%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/ChangeTracker.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils}/DiffUtils.java (72%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogCleaner.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatch.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatchApplier.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DataSourceFactory.java rename extensions/{broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config}/DslContextFactory.java (67%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/FlywayService.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java rename extensions/{broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRef.java} (51%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorStatusUpdater.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers}/ContractOfferQueries.java (56%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers}/ContractOfferRecordUpdater.java (52%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers}/DataOfferQueries.java (59%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler}/dao/utils/JsonbUtils.java (94%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler}/dao/utils/PostgresqlUtils.java (92%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/RecordPatch.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfig.java} (72%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfigFactory.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/config/EdcConfigPropertyUtils.java (83%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueue.java rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/queue/ConnectorQueueFiller.java (65%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/queue/ConnectorRefreshPriority.java (76%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/queue/ThreadPool.java (74%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/queue/ThreadPoolTask.java (86%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/queue/ThreadPoolTaskQueue.java (84%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/schedules/DeadConnectorRefreshJob.java (70%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorCleanerJob.java} (58%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/schedules/OfflineConnectorRefreshJob.java (70%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/schedules/OnlineConnectorRefreshJob.java (70%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/schedules/QuartzScheduleInitializer.java (90%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/schedules/utils/CronJobRef.java (75%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration}/schedules/utils/JobFactoryImpl.java (95%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler}/utils/CollectionUtils2.java (82%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler}/utils/JsonUtils2.java (74%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler}/utils/MapUtils.java (95%) rename extensions/{broker-server/src/main/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler}/utils/StringUtils2.java (60%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler}/AssertionUtils.java (83%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/JsonTestUtils.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLoggerTest.java rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriterTest.java} (80%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/DataOfferLimitsEnforcerTest.java (81%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/DataOfferWriterTestDataHelper.java (53%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/DataOfferWriterTestDataModels.java (95%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/DataOfferWriterTestResultHelper.java (78%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing}/DiffUtilsTest.java (79%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRefTest.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolQueueTest.java create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRemovalJobTest.java rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler}/utils/CollectionUtils2Test.java (62%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler}/utils/JsonUtils2Test.java (95%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver => catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler}/utils/StringUtils2Test.java (68%) create mode 100644 extensions/catalog-crawler/catalog-crawler/src/test/resources/logging.properties rename extensions/{broker-server-postgres-flyway-jooq => postgres-flyway-core}/README.md (56%) create mode 100644 extensions/postgres-flyway-core/build.gradle.kts create mode 100644 extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java create mode 100644 extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayUtils.java rename extensions/{broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java => postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/HikariDataSourceFactory.java} (59%) rename extensions/{broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java => postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/JdbcCredentials.java} (53%) rename launchers/{.env.broker => .env.catalog-crawler} (58%) delete mode 100644 launchers/connectors/broker-server-ce/build.gradle.kts delete mode 100644 launchers/connectors/broker-server-dev/build.gradle.kts create mode 100644 launchers/connectors/catalog-crawler-ce/build.gradle.kts create mode 100644 launchers/connectors/catalog-crawler-dev/build.gradle.kts create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java create mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java diff --git a/.dockerignore b/.dockerignore index 9c895ca99..db4a640cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -39,7 +39,7 @@ build .env !launchers/.env.connector -!launchers/.env.broker +!launchers/.env.catalog-crawler # Log files *.log diff --git a/.editorconfig b/.editorconfig index 5d2208c46..41861c3d0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,17 @@ indent_size = 2 [*.{yml,yaml}] indent_size = 2 +[*.java] +max_line_length = 140 +ij_continuation_indent_size = 8 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_line_comment_add_space = true +ij_java_imports_layout = *,|,java.**,javax.**,|,$* +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_doc_align_param_comments = false + + [{*.kt,*.kts}] ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ ij_kotlin_packages_to_use_import_on_demand = explicitly-none diff --git a/.env b/.env index 0aa50efab..e415c2283 100644 --- a/.env +++ b/.env @@ -3,4 +3,3 @@ EDC_IMAGE=ghcr.io/sovity/edc-dev:8.1.0 TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:8.1.0 EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:3.2.2 EDC_UI_ACTIVE_PROFILE=sovity-open-source -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:8.1.0 diff --git a/.env.dev b/.env.dev index c6574ad4f..498abf37b 100644 --- a/.env.dev +++ b/.env.dev @@ -3,4 +3,3 @@ EDC_IMAGE=ghcr.io/sovity/edc-dev:latest TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:latest EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:latest EDC_UI_ACTIVE_PROFILE=sovity-open-source -BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 1e37fe6e0..466f65ea1 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -46,8 +46,6 @@ Feel free to edit this release checklist in-progress depending on what tasks nee the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] Set the version for `TEST_BACKEND_IMAGE` of the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - - [ ] Set the version for `BROKER_IMAGE` of - the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] Set the UI release version for `EDC_UI_IMAGE` of the [docker-compose's .env file](https://github.com/sovity/edc-ce/blob/main/.env). - [ ] If the Eclipse EDC version changed, update @@ -58,13 +56,10 @@ Feel free to edit this release checklist in-progress depending on what tasks nee - [ ] Validate the image - [ ] Pull the latest edc-dev image: `docker image pull ghcr.io/sovity/edc-dev:latest`. - [ ] Check that your image was built recently `docker image ls | grep ghcr.io/sovity/edc-dev`. - - [ ] Test the release `docker-compose.yaml` with `EDC_IMAGE=ghcr.io/sovity/edc-dev:latest` and `BROKER_IMAGE=ghcr.io/sovity/broker-server-dev:latest` (at minimum execute a transfer between the two connectors). + - [ ] Test the release `docker-compose.yaml` with `EDC_IMAGE=ghcr.io/sovity/edc-dev:latest` (at minimum execute a transfer between the two connectors). - EDC - [ ] Test with `EDC_UI_ACTIVE_PROFILE=sovity-open-source` - [ ] Test with `EDC_UI_ACTIVE_PROFILE=mds-open-source` - - Broker - - [ ] Validate that the EDC is scanned. - - [ ] Validate that the index is searchable. - [ ] Ensure with a `docker ps -a` that all containers are healthy, and not `healthy: starting` or `healthy: unhealthy`. - [ ] [Create a release](https://github.com/sovity/edc-ce/releases/new) - [ ] In `Choose the tag`, type your new release version in the format `vx.y.z` (for instance `v1.2.3`) then click `+Create new tag vx.y.z on release`. diff --git a/.github/actions/build-connector-image/action.yml b/.github/actions/build-connector-image/action.yml index e9d991b56..099207c05 100644 --- a/.github/actions/build-connector-image/action.yml +++ b/.github/actions/build-connector-image/action.yml @@ -21,7 +21,7 @@ inputs: description: "EDC Connector Name in launchers/connectors/{connector-name}" deployment-type: required: true - description: "Type of deployment: 'connector' or 'broker'" + description: "Type of deployment: 'connector' or 'catalog-crawler'" title: required: true description: "Docker Image Title" diff --git a/.github/markdown-link-checker-config.jq b/.github/markdown-link-checker-config.jq index 8aec895b1..e73dc13c3 100755 --- a/.github/markdown-link-checker-config.jq +++ b/.github/markdown-link-checker-config.jq @@ -7,7 +7,6 @@ {"pattern": "^https://www\\.linkedin\\.com"}, {"pattern": "https://(.*?)\\.azure\\.sovity\\.io"}, {"pattern": "http://edc2?:"}, - {"pattern": "^https?://broker:"}, {"pattern": "^https?://connector:"} ], "replacementPatterns": [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dbf0dc93..d4ffe0c52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,30 +104,30 @@ jobs: deployment-type: "connector" title: "Test Data Source / Data Sink" description: "Provides a minimal data source / data sink for E2E tests." - - name: "Docker Image: broker-server-dev" + - name: "Docker Image: catalog-crawler-dev" uses: ./.github/actions/build-connector-image with: registry-url: ${{ env.REGISTRY_URL }} registry-user: ${{ env.REGISTRY_USER }} registry-password: ${{ secrets.GITHUB_TOKEN }} image-base-name: ${{ env.IMAGE_BASE_NAME }} - image-name: "broker-server-dev" - connector-name: "broker-server-dev" - deployment-type: "broker" - title: "Broker Server (Dev)" - description: "sovity EDC Broker Server. This dev version contains no auth and can be used to quickly start a locally running Broker Server + Broker UI." - - name: "Docker Image: broker-server-ce" + image-name: "catalog-crawler-dev" + connector-name: "catalog-crawler-dev" + deployment-type: "catalog-crawler" + title: "Catalog Crawler (Dev)" + description: "sovity CE Catalog crawler for the sovity CE Authority Portal. This dev version contains no auth and can be used to quickly start a locally running Catalog Crawler." + - name: "Docker Image: catalog-crawler-ce" uses: ./.github/actions/build-connector-image with: registry-url: ${{ env.REGISTRY_URL }} registry-user: ${{ env.REGISTRY_USER }} registry-password: ${{ secrets.GITHUB_TOKEN }} image-base-name: ${{ env.IMAGE_BASE_NAME }} - image-name: "broker-server-ce" - connector-name: "broker-server-ce" - deployment-type: "broker" - title: "Broker Server (Community Edition)" - description: "sovity EDC Broker Server. Requires dataspace credentials to join an existing dataspace." + image-name: "catalog-crawler-ce" + connector-name: "catalog-crawler-ce" + deployment-type: "catalog-crawler" + title: "Catalog Crawler (Community Edition, DAPS)" + description: "sovity CE Catalog crawler for the sovity CE Authority Portal. Requires DAPS dataspace credentials to join an existing dataspace." ts-api-client-library: name: TS API Client Library runs-on: ubuntu-latest @@ -181,6 +181,7 @@ jobs: env: NODE_USER: richardtreier-sovity NODE_AUTH_TOKEN: ${{ secrets.SOVITY_EDC_CLIENT_NPM_AUTH }} +<<<<<<< HEAD <<<<<<< HEAD ts-broker-api-client-library: runs-on: ubuntu-latest @@ -255,3 +256,5 @@ jobs: ======= NODE_AUTH_TOKEN: ${{ secrets.SOVITY_BROKER_CLIENT_NPM_AUTH }} >>>>>>> a68f9ccb (chore: fix the Broker's NPM publishing (#961)) +======= +>>>>>>> e3f28ff3 (feat: authority portal catalog crawler (#985)) diff --git a/.github/workflows/license_scan.yml b/.github/workflows/license_scan.yml index 84d865164..fff50cc76 100644 --- a/.github/workflows/license_scan.yml +++ b/.github/workflows/license_scan.yml @@ -31,8 +31,6 @@ jobs: run: cd extensions/wrapper/clients/typescript-client && npm clean-install - name: npm install (typescript-client-example) run: cd extensions/wrapper/clients/typescript-client-example && npm clean-install - - name: npm install (client-ts) - run: cd extensions/broker-server-api/client-ts && npm clean-install - name: Run license scanner uses: aquasecurity/trivy-action@master with: diff --git a/.gitignore b/.gitignore index 2d9fa213e..a2f744ea6 100644 --- a/.gitignore +++ b/.gitignore @@ -40,14 +40,13 @@ build **/.env !.env <<<<<<< HEAD -<<<<<<< HEAD !launchers/.env ======= !connector/.env >>>>>>> refs/rewritten/Merge-unrelated-tree-from-broker-into-edc-extensions ======= !launchers/.env.connector -!launchers/.env.broker +!launchers/.env.catalog-crawler >>>>>>> f6fe5d0e (Integrate the broker's extensions (#921)) # Log files diff --git a/CHANGELOG.md b/CHANGELOG.md index 4507377d1..06b4f7b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Overview -### EDC UI - -### EDC Extensions and Broker +### Detailed Changes #### Major Changes +- The Broker has been removed in favor of the Authority Portal and the new Deployment Unit, the "Data Catalog Crawler": + - Each "Data Catalog Crawler" connects to an existing Authority Portal Deployment's DB. + - Each "Data Catalog Crawler" is responsible for crawling exactly one environment. + - The Data Catalog functionality of the Broker has been integrated into the Authority Portal. - API Wrapper UI API: Moved to well-typed data sources, breaking changes to the asset model and API. #### Minor Changes @@ -25,8 +27,14 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Deployment Migration Notes -The database migration system has been moved from multiple migration history tables to a single one. Although this -process has been extensively tested in the enterprise edition already, it should be tested once more on a copy of a productive connector. +- Connector: + - The database migration system has been moved from multiple migration history tables to a single one. Although this + process has been extensively tested in the enterprise edition already, it should be tested once more on a copy of a productive connector. +- Broker: + - The broker has been removed. For Authority Portal users, please check out the new + [Data Catalog Crawler Productive Deployment Guide](docs/deployment-guide/goals/catalog-crawler-production/README.md). + - Any previous broker deployment's database is not required anymore. + - Please care that only some environment variables look similar. It is recommended to create fresh deployments. ## [8.1.0] - 2024-06-14 @@ -42,7 +50,7 @@ Support for Multiplicity Constraints in the API Wrapper. #### Minor Changes -- API Wrapper +- API Wrapper - Support for Multiplicity Constraints (https://github.com/sovity/edc-ce/issues/968) - Providing `Prop` class from `json-and-jsonld-utils` to the java-client to make relevant Constants available @@ -88,7 +96,6 @@ The functionalities of each part, Broker and Extensions, on this release, is the - Broker CE: `ghcr.io/sovity/broker-server-ce:8.0.0` - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` - ## [7.5.0] - 2024-05-16 ### Overview @@ -114,9 +121,9 @@ Security updates #### Compatible Versions - Connector Backend Docker Images: - - Dev EDC: `ghcr.io/sovity/edc-dev:7.5.0` - - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.5.0` - - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.5.0` + - Dev EDC: `ghcr.io/sovity/edc-dev:7.5.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.5.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.5.0` - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` - Connector UI Release: https://github.com/sovity/edc-ui/releases/tag/v3.2.2 @@ -146,9 +153,9 @@ Contains DB migrations, DB backups advised. #### Compatible Versions - Connector Backend Docker Images: - - Dev EDC: `ghcr.io/sovity/edc-dev:7.4.2` - - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.4.2` - - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.4.2` + - Dev EDC: `ghcr.io/sovity/edc-dev:7.4.2` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:7.4.2` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:7.4.2` - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:3.2.2` - Connector UI Release: https://github.com/sovity/edc-ui/releases/tag/v3.2.2 diff --git a/archived/broker/README.md b/archived/broker/README.md deleted file mode 100644 index 93e9ed983..000000000 --- a/archived/broker/README.md +++ /dev/null @@ -1,246 +0,0 @@ - - - - - -
      -
      - -Logo - - -

      Broker Server

      -

      -Broker Backend & EDC Extensions. -
      -Report Bug -· -Request Feature -

      -
      - - -
      - Table of Contents -
        -
      1. About The Project
      2. -
      3. Development
      4. -
      5. Releasing
      6. -
      7. Deployment
      8. -
      9. License
      10. -
      11. Contact
      12. -
      -
      - -## About The Project - -[Eclipse Dataspace Components](https://github.com/eclipse-edc) (EDC) is a framework -for building dataspaces, exchanging data securely with ensured data sovereignty. - -[sovity](https://sovity.de/) extends the EDC Connector's functionality with extensions to offer -enterprise-ready managed services like "Connector-as-a-Service", out-of-the-box fully configured DAPS -and integrations to existing other dataspace technologies. - -An IDS Broker is a central component of a dataspace that operates on the IDS protocol, that aggregates and indexes -connectors and data offers. - -This IDS Broker is written on basis of the EDC and should be used in tandem with the Broker UI. - -

      (back to top)

      - -## Development - -### Local Development - -#### Local Backend Development - -For local backend development, access to the GitHub Maven Registry is required. - -To access the GitHub Maven Registry you need to provide the following properties, e.g. by providing -a `~/.gradle/gradle.properties`. - -```properties -gpr.user={your github username} -gpr.key={your github pat with packages.read} -``` - -Developing the Broker Backend tests are used to validate functionality: - -- There are Integration Tests using the Broker Server Java Client Library for testing API Endpoints of a running - backend. -- There are Integration Tests using the Broker Server Java Client Library and sovity EDC Extensions to integration - test the Broker with a running EDC where communication works through the Data Space Protocol (DSP). -- There are Unit Tests with Mockito for testing local complexity, e.g. mappers, data structures, utilities. - -

      (back to top)

      - -#### Local UI Development - -The Broker UI is a profile `broker` of the [EDC UI](https://github.com/sovity/edc-ui): - -The Broker UI depends on the NPM -Package [@sovity/broker-server-client](https://www.npmjs.com/package/@sovity.de/broker-server-client) built on the main -branch or on releases. - -Local Broker UI Development can start with the type-safe broker server fake backend once the Client Library version is -bumped to contain the up-to-date API Models. - -

      (back to top)

      - -### Local E2E Development - -There is currently no support for Local E2E Development (a locally running backend build server and a locally running -frontend build server). - -For debugging UI issues, however, the UI can be manually configured to use a live backend, e.g. one started via -the [docker-compose.yaml](#local-demo). - -

      (back to top)

      - -### Local Demo - -There is a [docker-compose.yaml](../../docker-compose.yaml) that starts a broker and a connector. - -At release time it is pinned down to the release versions. - -Mid-development it might be un-pinned back to latest versions. - -| | Broker | Conncetor | -|---------------------|------------------------------------------------------------------|:-----------------------------------------------------------------------------| -| Homepage | http://localhost:11000 | http://localhost:22000 | -| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | -| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | -| Connector Endpoint | http://broker:11003/api/dsp
      Requires Docker Compose Network | http://connector:22003/api/dsp
      Requires Docker Compose Network | - -

      (back to top)

      - -## Releasing - -[Create a Release Issue](https://github.com/sovity/edc-ce/issues/new?assignees=&labels=task%2Frelease%2Cscope%2Fmds&projects=&template=release.md&title=Release+x.x.x) and follow the instructions. - -

      (back to top)

      - -## Deployment - -### Deployment Units - -| Deployment Unit | Version / Details | -|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | -| Postgresql | 15 or compatible version | -| Broker Backend | broker-server-ce, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/v4.2.0/CHANGELOG.md) for compatible versions. | -| Broker UI | edc-ui, see [CHANGELOG.md](../../CHANGELOG.md) for version 8+ or [former CHANGELOG.md](https://github.com/sovity/edc-broker-server-extension/blob/v4.2.0/CHANGELOG.md) for compatible versions. | - -### Configuration - -There is a [docker-compose.yaml](../../docker-compose.yaml) to try out the broker locally. However, a productive release will require a few more configuration options, so you should only use it to check if the released version is roughly working or if it's broken. - -#### Reverse Proxy Configuration - -- The broker is meant to be served via TLS/HTTPS. -- The broker is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `8080` port. - - The Backend's `11002` port. - - The Backend's `11003` port. -- The mapping should look like this: - - `https://[MY_EDC_FQDN]/backend/api/dsp` -> `broker-backend:11003/backend/api/dsp` - - `https://[MY_EDC_FQDN]/backend/api/management` -> `broker-backend:11002/backend/api/management` - - All other requests -> `broker-ui:8080` - -#### Backend Configuration - -A productive configuration will require you to join a DAPS. - -For that you will need a SKI/AKI ClientID. Please refer -to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-ce/tree/main/docs/getting-started#faq) -on how to generate one. - -The DAPS needs to contain the claim `referringConnector=broker` for the broker. -The expected value `broker` could be overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. - -```yaml -# Required: Fully Qualified Domain Name -MY_EDC_FQDN: "example.com" - -# Required: DB -MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc -MY_EDC_JDBC_USER: edc -MY_EDC_JDBC_PASSWORD: edc - -# Required: List of EDCs to fetch -EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/api/dsp,https://connector-b/api/dsp" - -# List of Data Space Names for special Connectors (default: '') -EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/api/dsp,OtherDataspace=https://some-other-connector/api/dsp" - -# Required: DAPS credentials -EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' -EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' -EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' -EDC_KEYSTORE: '_your keystore file_' # Needs to be available as file in the running container -EDC_KEYSTORE_PASSWORD: '_your keystore password_' -EDC_OAUTH_CERTIFICATE_ALIAS: 1 -EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 - -# Required: Management API Key -EDC_API_AUTH_KEY: "ApiKeyDefaultValue" - -# Required: Admin Api Key -EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey -``` - -All pre-configured config values for either the broker server or the underlying EDC can be found -in [launchers/.env.broker](../../launchers/.env.broker). - -#### UI Configuration - -```yaml -# Required: Profile -EDC_UI_ACTIVE_PROFILE: broker - -# Required: Management API URL -EDC_UI_MANAGEMENT_API_URL: https://my-broker.com/backend/api/management - -# Required: Management API Key -EDC_UI_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" -``` - -#### Adding Connectors at runtime - -Connectors can be dynamically added at runtime by using the following endpoint: - -```shell script -# Response should be 204 No Content -curl --request PUT \ - --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ - --header 'Content-Type: application/json' \ - --header 'x-api-key: ApiKeyDefaultValue' \ - --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' -``` - -#### Removing Connectors at runtime - -Connectors can be dynamically removed at runtime by using the following endpoint: - -```shell script -# Response should be 204 No Content -curl --request DELETE \ - --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ - --header 'Content-Type: application/json' \ - --header 'x-api-key: ApiKeyDefaultValue' \ - --data '["https://some-connector-to-be-removed/api/dsp", "https://some-other-connector-to-be-removed/api/dsp"]' -``` - -

      (back to top)

      - -## License - -Distributed under the Apache 2.0 License. See `LICENSE` for more information. - -

      (back to top)

      - -## Contact - -contact@sovity.de - -

      (back to top)

      diff --git a/build.gradle.kts b/build.gradle.kts index 02b11e740..f9ff62348 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -122,6 +122,12 @@ subprojects { } } + tasks.register("printClasspath") { + group = libs.versions.edcGroup.get() + description = "The EdcRuntimeExtension JUnit Extension requires the gradle task 'printClasspath'" + println(sourceSets.main.get().runtimeClasspath.asPath); + } + java { withSourcesJar() withJavadocJar() diff --git a/connector/README.md b/connector/README.md deleted file mode 100644 index 95b296ad4..000000000 --- a/connector/README.md +++ /dev/null @@ -1,39 +0,0 @@ - -
      -
      - - Logo - - -

      Broker Server:
      Docker Images

      - -

      - Report Bug - · - Request Feature -

      -
      - -## Image Variants - -The Broker Server is built in different variants: - -| Docker Image | Type | Purpose | Features | -|----------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| -| [broker-server-dev](https://github.com/sovity/edc-ce/pkgs/container/broker-server-dev) | Development |
      • Local Deployment via our `docker-compose.yaml`
      • E2E Testing
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • Mock IAM
      | -| [broker-server-ce](https://github.com/sovity/edc-ce/pkgs/container/broker-server-ce) | Community Edition |
      • Productive Deployment
      |
      • Broker Server Extension(s)
      • PostgreSQL Persistence & Flyway
      • DAPS Authentication
      | - -## Image Tags - -| Tag | Description | -|---------------|-----------------------------------| -| latest / main | latest version of our main branch | -| release | latest release of this repository | - -## License - -Apache License 2.0 - see [LICENSE](../LICENSE) - -## Contact - -sovity GmbH - contact@sovity.de diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 0789fdcfb..01d93ec40 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -101,53 +101,6 @@ services: ports: - '33001:11001' - broker-ui: - image: ${EDC_UI_IMAGE} - ports: - - '44000:8080' - environment: - EDC_UI_ACTIVE_PROFILE: broker - EDC_UI_MANAGEMENT_API_URL: http://localhost:44002/backend/api/management - EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue - NGINX_ACCESS_LOG: off - - broker: - image: ${BROKER_IMAGE} - depends_on: - broker-postgresql: - condition: service_healthy - edc: - condition: service_started - edc2: - condition: service_started - environment: - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://edc:11003/api/dsp,http://edc2:11003/api/dsp" - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" - - # Hide offline data offers after 1 minute in dev - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" - - MY_EDC_FQDN: "broker" - EDC_API_AUTH_KEY: ApiKeyDefaultValue - - MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc - MY_EDC_JDBC_USER: edc - MY_EDC_JDBC_PASSWORD: edc - - # docker compose local dev environment overrides (don't use with non-dev images) - MY_EDC_PROTOCOL: "http://" - EDC_DSP_CALLBACK_ADDRESS: http://broker:44003/backend/api/dsp - EDC_WEB_REST_CORS_ENABLED: 'true' - EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' - EDC_WEB_REST_CORS_ORIGINS: '*' - EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work - ports: - - '44001:11001' - - '44002:11002' - - '44003:11003' - - '44004:11004' - - '44005:5005' - postgresql: image: docker.io/bitnami/postgresql:15 restart: always @@ -182,27 +135,8 @@ services: timeout: 5s retries: 10 - broker-postgresql: - image: docker.io/bitnami/postgresql:15 - restart: always - environment: - POSTGRESQL_USERNAME: edc - POSTGRESQL_PASSWORD: edc - POSTGRESQL_DATABASE: edc - ports: - - '54323:5432' - volumes: - - 'broker-postgresql:/bitnami/postgresql' - healthcheck: - test: ["CMD-SHELL", "pg_isready -U edc"] - interval: 1s - timeout: 5s - retries: 10 - volumes: postgresql: driver: local postgresql2: driver: local - broker-postgresql: - driver: local diff --git a/docker-compose.yaml b/docker-compose.yaml index 0789fdcfb..01d93ec40 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -101,53 +101,6 @@ services: ports: - '33001:11001' - broker-ui: - image: ${EDC_UI_IMAGE} - ports: - - '44000:8080' - environment: - EDC_UI_ACTIVE_PROFILE: broker - EDC_UI_MANAGEMENT_API_URL: http://localhost:44002/backend/api/management - EDC_UI_MANAGEMENT_API_KEY: ApiKeyDefaultValue - NGINX_ACCESS_LOG: off - - broker: - image: ${BROKER_IMAGE} - depends_on: - broker-postgresql: - condition: service_healthy - edc: - condition: service_started - edc2: - condition: service_started - environment: - EDC_BROKER_SERVER_KNOWN_CONNECTORS: "http://edc:11003/api/dsp,http://edc2:11003/api/dsp" - EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-other-connector/api/dsp" - - # Hide offline data offers after 1 minute in dev - EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER: "PT1M" - - MY_EDC_FQDN: "broker" - EDC_API_AUTH_KEY: ApiKeyDefaultValue - - MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc - MY_EDC_JDBC_USER: edc - MY_EDC_JDBC_PASSWORD: edc - - # docker compose local dev environment overrides (don't use with non-dev images) - MY_EDC_PROTOCOL: "http://" - EDC_DSP_CALLBACK_ADDRESS: http://broker:44003/backend/api/dsp - EDC_WEB_REST_CORS_ENABLED: 'true' - EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' - EDC_WEB_REST_CORS_ORIGINS: '*' - EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work - ports: - - '44001:11001' - - '44002:11002' - - '44003:11003' - - '44004:11004' - - '44005:5005' - postgresql: image: docker.io/bitnami/postgresql:15 restart: always @@ -182,27 +135,8 @@ services: timeout: 5s retries: 10 - broker-postgresql: - image: docker.io/bitnami/postgresql:15 - restart: always - environment: - POSTGRESQL_USERNAME: edc - POSTGRESQL_PASSWORD: edc - POSTGRESQL_DATABASE: edc - ports: - - '54323:5432' - volumes: - - 'broker-postgresql:/bitnami/postgresql' - healthcheck: - test: ["CMD-SHELL", "pg_isready -U edc"] - interval: 1s - timeout: 5s - retries: 10 - volumes: postgresql: driver: local postgresql2: driver: local - broker-postgresql: - driver: local diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 8c5292975..c5531ca25 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -524,11 +524,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource + default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM - default: CUSTOM SecretValue: type: object properties: @@ -736,6 +736,7 @@ components: UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource + default: GET enum: - GET - POST @@ -743,7 +744,6 @@ components: - PATCH - DELETE - OPTIONS - default: GET UiDataSourceOnRequest: required: - contactEmail @@ -1416,7 +1416,7 @@ components: ContractAgreementPageQuery: type: object properties: - status: + terminationStatus: type: string description: Optionally filter the resulting contract agreements by their termination status. diff --git a/docs/deployment-guide/goals/broker-production/README.md b/docs/deployment-guide/goals/broker-production/README.md deleted file mode 100644 index dbc3da385..000000000 --- a/docs/deployment-guide/goals/broker-production/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Broker Productive Deployment Guide - -## About this Guide - -This is a productive deployment guide for self-hosting a functional sovity Broker. - -## Deployment Units - -| Deployment Unit | Version / Details | -|----------------------------------------------------------------|-----------------------------------------------------------------------------------------| -| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | -| Postgresql | 15 or compatible version | -| Broker Backend | broker-server-ce, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | -| Broker UI | edc-ui, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | - -### Configuration - -There is a [docker-compose.yaml](../../../../docker-compose.yaml) to try out the broker locally. -However, a productive release will require a few more configuration options, -so you should only use it to check if the released version is roughly working or if it's broken. - -#### Reverse Proxy Configuration - -- The broker is meant to be served via TLS/HTTPS. -- The broker is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `8080` port. - - The Backend's `11002` port. - - The Backend's `11003` port. -- The mapping should look like this: - - `https://[MY_EDC_FQDN]/backend/api/dsp` -> `broker-backend:11003/backend/api/dsp` - - `https://[MY_EDC_FQDN]/backend/api/management` -> `broker-backend:11002/backend/api/management` - - All other requests -> `broker-ui:8080` - -#### Backend Configuration - -A productive configuration will require you to join a DAPS. - -For that you will need a SKI/AKI ClientID. Please refer -to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-ce/tree/main/docs/getting-started#faq) -on how to generate one. - -The DAPS needs to contain the claim `referringConnector=broker` for the broker. -The expected value `broker` could be overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. - -```yaml -# Required: Fully Qualified Domain Name -MY_EDC_FQDN: "example.com" - -# Required: DB -MY_EDC_JDBC_URL: jdbc:postgresql://broker-postgresql:5432/edc -MY_EDC_JDBC_USER: edc -MY_EDC_JDBC_PASSWORD: edc - -# Required: List of EDCs to fetch -EDC_BROKER_SERVER_KNOWN_CONNECTORS: "https://connector-a/api/dsp,https://connector-b/api/dsp" - -# List of Data Space Names for special Connectors (default: '') -EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS: "Mobilithek=https://some-connector/api/dsp,OtherDataspace=https://some-other-connector/api/dsp" - -# Required: DAPS credentials -EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' -EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' -EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' -EDC_KEYSTORE: '_your keystore file_' # Needs to be available as file in the running container -EDC_KEYSTORE_PASSWORD: '_your keystore password_' -EDC_OAUTH_CERTIFICATE_ALIAS: 1 -EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 - -# Required: Management API Key -EDC_API_AUTH_KEY: "ApiKeyDefaultValue" - -# Required: Admin Api Key -EDC_BROKER_SERVER_ADMIN_API_KEY: DefaultBrokerServerAdminApiKey -``` - -All pre-configured config values for either the broker server or the underlying EDC can be found -in [launcher/.env.broker](../../../../launchers/.env.broker). - -#### UI Configuration - -```yaml -# Required: Profile -EDC_UI_ACTIVE_PROFILE: broker - -# Required: Management API URL -EDC_UI_MANAGEMENT_API_URL: https://my-broker.com/backend/api/management - -# Required: Management API Key -EDC_UI_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" -``` - -#### Adding Connectors at runtime - -Connectors can be dynamically added at runtime by using the following endpoint: - -```shell script -# Response should be 204 No Content -curl --request PUT \ - --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ - --header 'Content-Type: application/json' \ - --header 'x-api-key: ApiKeyDefaultValue' \ - --data '["https://some-new-connector/api/dsp", "https://some-other-new-connector/api/dsp"]' -``` - -#### Removing Connectors at runtime - -Connectors can be dynamically removed at runtime by using the following endpoint: - -```shell script -# Response should be 204 No Content -curl --request DELETE \ - --url 'http://localhost:11002/backend/api/management/wrapper/broker/connectors?adminApiKey=DefaultBrokerServerAdminApiKey' \ - --header 'Content-Type: application/json' \ - --header 'x-api-key: ApiKeyDefaultValue' \ - --data '["https://some-connector-to-be-removed/api/dsp", "https://some-other-connector-to-be-removed/api/dsp"]' -``` - -

      (back to top)

      - -## License - -Distributed under the Apache 2.0 License. See `LICENSE` for more information. - -

      (back to top)

      - -## Contact - -contact@sovity.de - -

      (back to top)

      diff --git a/docs/deployment-guide/goals/catalog-crawler-production/README.md b/docs/deployment-guide/goals/catalog-crawler-production/README.md new file mode 100644 index 000000000..5bbbddecf --- /dev/null +++ b/docs/deployment-guide/goals/catalog-crawler-production/README.md @@ -0,0 +1,55 @@ +# Catalog Crawler Productive Deployment Guide + +## About this Guide + +This is a productive deployment guide for self-hosting a single [catalog crawler](../../../../extensions/catalog-crawler/README.md). + +One catalog crawler per Authority Portal Deployment Environment must be deployed. + +## Deployment Units + +| Deployment Unit | Version / Details | +|-----------------|---------------------------------------------------------------------------------------------------------| +| Catalog Crawler | see the changelog for available versions. See the Authority Portal's Changelog for compatible versions. | + +#### Reverse Proxy Configuration + +- The catalog crawler is meant to be served via TLS/HTTPS. +- The catalog crawler is meant to be deployed with a reverse proxy terminating TLS / providing HTTPS. +- All requests are meant to be redirected to the deployment's `11003` port. + +#### Catalog Crawler Configuration + +A productive configuration will require you to join a DAPS. + +For that you will need a SKI/AKI client ID. Please refer +to [edc-extension's Getting Started Guide](https://github.com/sovity/edc-ce/tree/main/docs/getting-started#faq) +on how to generate one. + +The DAPS needs to contain the claim `referringConnector=broker` for the broker. +The expected value `broker` could be overridden by specifying a different value for `MY_EDC_PARTICIPANT_ID`. + +```yaml +# Required: Fully Qualified Domain Name +MY_EDC_FQDN: "crawler.test.example.com" + +# Required: Authority Portal Environment ID +CRAWLER_ENVIRONMENT_ID: test + +# Required: Authority Portal Postgresql DB Access +CRAWLER_DB_JDBC_URL: jdbc:postgresql://authority-portal:5432/portal +CRAWLER_DB_JDBC_USER: portal +CRAWLER_DB_JDBC_PASSWORD: portal + +# Required: DAPS credentials +EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' +EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' +EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' +EDC_KEYSTORE: '_your keystore file_' # Needs to be available as file in the running container +EDC_KEYSTORE_PASSWORD: '_your keystore password_' +EDC_OAUTH_CERTIFICATE_ALIAS: 1 +EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 +``` + +All pre-configured config values for either the catalog crawler can be found +in [launcher/.env.catalog-crawler](../../../../launchers/.env.catalog-crawler). diff --git a/docs/deployment-guide/goals/local-demo/README.md b/docs/deployment-guide/goals/local-demo/README.md index fe90159a3..d52d6c1f5 100644 --- a/docs/deployment-guide/goals/local-demo/README.md +++ b/docs/deployment-guide/goals/local-demo/README.md @@ -52,15 +52,13 @@ EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up ## Quick Start: Default Configuration -The default configuration launches two local EDC Connectors and a Broker with the following credentials: - -| | First Connector | Second Connector | Broker | -|---------------------|---------------------------------------------------------------|:---------------------------------------------------------------|------------------------------------------------------------------| -| Homepage | http://localhost:11000 | http://localhost:22000 | http://localhost:44000 | -| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | http://localhost:44002/api/management | -| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | -| Connector Endpoint | http://edc:11003/api/dsp
      Requires Docker Compose Network | http://edc2:11003/api/dsp
      Requires Docker Compose Network | http://broker:11003/api/dsp
      Requires Docker Compose Network | - -The Broker is configured to scan both connectors. +The default configuration launches two local EDC Connectors with the following credentials: + +| | First Connector | Second Connector | +|---------------------|---------------------------------------------------------------|:---------------------------------------------------------------| +| Homepage | http://localhost:11000 | http://localhost:22000 | +| Management Endpoint | http://localhost:11002/api/management | http://localhost:22002/api/management | +| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | +| Connector Endpoint | http://edc:11003/api/dsp
      Requires Docker Compose Network | http://edc2:11003/api/dsp
      Requires Docker Compose Network |

      (back to top)

      diff --git a/docs/deployment-guide/goals/production/4.2.0/README.md b/docs/deployment-guide/goals/production/4.2.0/README.md index 59adef4aa..ee9f58d48 100644 --- a/docs/deployment-guide/goals/production/4.2.0/README.md +++ b/docs/deployment-guide/goals/production/4.2.0/README.md @@ -171,13 +171,12 @@ You can use a script (if you're on WSL or Linux) to generate the SKI, AKI and jk No, locally run connectors cannot exchange data with online connectors. A connector must have a proper URL + configuration and be accessible from the data provider via REST calls. -### (MDS Only) Can I disable the Broker- and/or ClearingHouse-Client-Extensions dynamically? +### (MDS Only) Can I disable the ClearingHouse-Client-Extensions dynamically? Yes, if the two extensions are included, they can still be disabled via properties. The default settings can be found in `docker-compose.yaml` and can be changed there. ```yaml # Extension Configuration -BROKER_CLIENT_EXTENSION_ENABLED: false # disabled by default CLEARINGHOUSE_CLIENT_EXTENSION_ENABLED: true # enabled by default ``` diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml index 334a1d067..cf56b4068 100644 --- a/docs/dev/checkstyle/checkstyle-config.xml +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -181,7 +181,7 @@ - + @@ -197,27 +197,27 @@ value="Member name ''{0}'' must match pattern ''{1}''."/> - + - + - + - + - + @@ -232,7 +232,7 @@ value="Class type name ''{0}'' must match pattern ''{1}''."/> - + diff --git a/extensions/broker-server-api/api/README.md b/extensions/broker-server-api/api/README.md deleted file mode 100644 index addf0780c..000000000 --- a/extensions/broker-server-api/api/README.md +++ /dev/null @@ -1,27 +0,0 @@ - -
      -
      - - Logo - - -

      Broker Server API Specification

      - -

      - Report Bug - · - Request Feature -

      -
      - -## About this component - -Specification of Broker Server API endpoints, for example endpoints for the Broker UI. - -## License - -Apache License 2.0 - see [LICENSE](../../../LICENSE) - -## Contact - -sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-api/api/build.gradle.kts b/extensions/broker-server-api/api/build.gradle.kts deleted file mode 100644 index a34c94429..000000000 --- a/extensions/broker-server-api/api/build.gradle.kts +++ /dev/null @@ -1,79 +0,0 @@ - -plugins { - `java-library` - `maven-publish` - alias(libs.plugins.swagger.plugin) //./gradlew clean resolve - alias(libs.plugins.hidetake.swaggerGenerator) //./gradlew generateSwaggerUI - alias(libs.plugins.openapi.generator7) //./gradlew openApiValidate && ./gradlew openApiGenerate -} - -dependencies { - annotationProcessor(libs.lombok) - compileOnly(libs.lombok) - - api(project(":extensions:wrapper:wrapper-common-api")) - - api(libs.jakarta.rsApi) - api(libs.jakarta.validationApi) - api(libs.swagger.annotationsJakarta) - api(libs.swagger.jaxrs2Jakarta) - api(libs.jakarta.servletApi) - - implementation(libs.apache.commonsLang) - implementation(libs.jakarta.validationApi) -} - -val openapiFileDir = project.layout.buildDirectory.get().asFile.resolve("swagger").path -val openapiFileFilename = "broker-server.yaml" -val openapiFile = "$openapiFileDir/$openapiFileFilename" - -tasks.withType { - outputDir = file(openapiFileDir) - outputFileName = openapiFileFilename.removeSuffix(".yaml") - prettyPrint = true - outputFormat = io.swagger.v3.plugins.gradle.tasks.ResolveTask.Format.YAML - classpath = java.sourceSets["main"].runtimeClasspath - buildClasspath = classpath - resourcePackages = setOf("") -} - -task("openApiGenerateTypeScriptClient") { - validateSpec.set(false) - dependsOn("resolve") - generatorName.set("typescript-fetch") - configOptions.set(mutableMapOf( - "supportsES6" to "true", - "npmVersion" to "8.15.0", - "typescriptThreePlus" to "true", - )) - - inputSpec.set(openapiFile) - val outputDirectory = buildFile.parentFile.resolve("../client-ts/src/generated").normalize() - outputDir.set(outputDirectory.toString()) - - doFirst { - project.delete(fileTree(outputDirectory).exclude("**/.gitignore")) - } - - doLast { - outputDirectory.resolve("src/generated").renameTo(outputDirectory) - } -} - -tasks.withType { - dependsOn("resolve") - dependsOn("openApiGenerateTypeScriptClient") - from(openapiFileDir) { - include(openapiFileFilename) - } -} - -group = libs.versions.sovityEdcExtensionGroup.get() - -publishing { - publications { - create(project.name) { - from(components["java"]) - } - } -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java deleted file mode 100644 index 81f868094..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/ApiInformation.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api; - -import io.swagger.v3.oas.annotations.ExternalDocumentation; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.info.License; - -@OpenAPIDefinition( - info = @Info( - title = "Broker Server API (Deprecated)", - version = "0.0.0", - description = "Broker Server API for the Broker Server built by sovity.", - contact = @Contact( - name = "sovity GmbH", - email = "contact@sovity.de", - url = "https://github.com/sovity/edc-ce/issues/new/choose" - ), - license = @License( - name = "Apache 2.0", - url = "https://github.com/sovity/edc-ce/blob/main/LICENSE" - ) - ), - externalDocs = @ExternalDocumentation( - description = "Broker Server API in sovity/ce", - url = "https://github.com/sovity/edc-broker-server-extension/tree/main/extensions/broker-server-api" - ) -) -public interface ApiInformation { -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java deleted file mode 100644 index 5bcbfca82..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/BrokerServerResource.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorCreationRequest; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; - -import java.util.List; - -@Path("wrapper/broker") -@Tag(name = "Broker Server", description = "Broker Server API Endpoints. Requires the Broker Server Extension") -public interface BrokerServerResource { - - @POST - @Path("catalog-page") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query the Broker's Catalog of Data Offers") - CatalogPageResult catalogPage(CatalogPageQuery query); - - @POST - @Path("connector-page") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query the List of Known Connectors") - ConnectorPageResult connectorPage(ConnectorPageQuery query); - - @POST - @Path("data-offer-detail-page") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query a Data Offer's Detail Page") - DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery query); - - @POST - @Path("connector-detail-page") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Query a known Connector's Detail Page") - ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query); - - @PUT - @Path("connectors") - @Consumes(MediaType.APPLICATION_JSON) - @Operation(description = "Add unknown Connectors to the Broker Server") - void addConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); - - @PUT - @Path("connectors-with-mdsid") - @Consumes(MediaType.APPLICATION_JSON) - @Operation(description = "Add unknown Connectors with MDS IDs to the Broker Server") - void addConnectorsWithMdsIds(ConnectorCreationRequest connectors, @QueryParam("adminApiKey") String adminApiKey); - - @DELETE - @Path("connectors") - @Consumes(MediaType.APPLICATION_JSON) - @Operation(description = "Delete known Connectors from the Broker Server") - void deleteConnectors(List endpoints, @QueryParam("adminApiKey") String adminApiKey); - - @POST - @Path("authority-portal-api/connectors") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Provide Connector metadata by provided Connector Endpoints") - List getConnectorMetadata(List endpoints, @QueryParam("adminApiKey") String adminApiKey); - - @POST - @Path("authority-portal-api/organization-metadata") - @Consumes(MediaType.APPLICATION_JSON) - @Operation(description = "Update organization metadata. Organizations not contained in the payload will be deleted.") - void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest organizationMetadataRequest, @QueryParam("adminApiKey") String adminApiKey); - - @POST - @Path("authority-portal-api/data-offer-info") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Provides information about Data Offers for given Connectors.") - List getConnectorDataOffers(List endpoints, @QueryParam("adminApiKey") String adminApiKey); -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java deleted file mode 100644 index 2a71d0e5e..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AddedConnector.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Information for adding an unknown connector.", requiredMode = Schema.RequiredMode.REQUIRED) -public class AddedConnector { - @Schema(description = "Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) - private String connectorEndpoint; - @Schema(description = "Organization MDS ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String mdsId; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java deleted file mode 100644 index 4ef3d92af..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferDetails.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Data offer details of a connector.") -public class AuthorityPortalConnectorDataOfferDetails { - @Schema(description = "Asset ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String dataOfferId; - - @Schema(description = "Name of the asset", requiredMode = Schema.RequiredMode.REQUIRED) - private String dataOfferName; - -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java deleted file mode 100644 index 13f4792eb..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorDataOfferInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Details of a Connector and its data offers.") -public class AuthorityPortalConnectorDataOfferInfo { - - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) - private String connectorEndpoint; - - @Schema(description = "ID of participant", requiredMode = Schema.RequiredMode.REQUIRED) - private String participantId; - - @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorOnlineStatus onlineStatus; - - @Schema(description = "Date to be displayed as last update date, for online connectors it's the last refresh date, for offline connectors it's the creation date or last successful fetch.") - private OffsetDateTime offlineSinceOrLastUpdatedAt; - - @Schema(description = "Available Data Offers", requiredMode = Schema.RequiredMode.REQUIRED) - private List dataOffers; - -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java deleted file mode 100644 index 7bc088765..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalConnectorInfo.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Information for one connector, as required for the Authority Portal.", requiredMode = Schema.RequiredMode.REQUIRED) -public class AuthorityPortalConnectorInfo { - @Schema(description = "Connector Endpoint", requiredMode = Schema.RequiredMode.REQUIRED) - private String connectorEndpoint; - @Schema(description = "Connector Participant ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String participantId; - @Schema(description = "Number of public Data Offers in this connector, as tracked by the broker", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer dataOfferCount; - @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorOnlineStatus onlineStatus; - @Schema(description = "Last successful refresh time stamp of the online status", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime offlineSinceOrLastUpdatedAt; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java deleted file mode 100644 index 5acd846e1..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadata.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2024 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Information about a single organization from the Authority Portal.") -public class AuthorityPortalOrganizationMetadata { - @Schema(description = "MDS-ID from the Authority Portal") - private String mdsId; - @Schema(description = "Company name") - private String name; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java deleted file mode 100644 index 68eb269eb..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/AuthorityPortalOrganizationMetadataRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Information about organizations from the Authority Portal.") -public class AuthorityPortalOrganizationMetadataRequest { - @Schema(description = "Organization metadata") - private List organizations; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java deleted file mode 100644 index 31f4b02a9..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogContractOffer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "A contract offer a data offer is available under (as required by the catalog).") -public class CatalogContractOffer { - @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String contractOfferId; - - @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime updatedAt; - - @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) - private UiPolicy contractPolicy; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java deleted file mode 100644 index 4c89b522b..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogDataOffer.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Data Offer, meaning an offered asset.") -public class CatalogDataOffer { - @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) - private String assetId; - - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) - private String connectorEndpoint; - - @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorOnlineStatus connectorOnlineStatus; - - @Schema(description = "Date to be displayed as last update date, for online connectors it's the last refresh date, for offline connectors it's the creation date or last successful fetch.") - private OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; - - @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime updatedAt; - - @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) - private UiAsset asset; - - @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) - private List contractOffers; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java deleted file mode 100644 index a69f19e37..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageQuery.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Filterable Catalog Page Query") -public class CatalogPageQuery { - @Schema(description = "Selected filters") - private CnfFilterValue filter; - - @Schema(description = "Search query") - private String searchQuery; - - @Schema(description = "Sorting") - private CatalogPageSortingType sorting; - - @Schema(description = "Page number, one based, meaning the first page is page 1.", example = "1", defaultValue = "1", type = "n") - private Integer pageOneBased; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java deleted file mode 100644 index 5b99be10e..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageResult.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Catalog Page and visible filters") -public class CatalogPageResult { - @Schema(description = "Available filter options", requiredMode = Schema.RequiredMode.REQUIRED) - private CnfFilter availableFilters; - - @Schema(description = "Available sorting options", requiredMode = Schema.RequiredMode.REQUIRED) - private List availableSortings; - - @Schema(description = "Pagination Metadata", requiredMode = Schema.RequiredMode.REQUIRED) - private PaginationMetadata paginationMetadata; - - @Schema(description = "Current page of data offers", requiredMode = Schema.RequiredMode.REQUIRED) - private List dataOffers; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java deleted file mode 100644 index ea1d1ed7d..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingItem.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Available Catalog Page Sorting Item") -public class CatalogPageSortingItem { - @Schema(description = "Sorting ID", requiredMode = Schema.RequiredMode.REQUIRED) - private CatalogPageSortingType sorting; - @Schema(description = "Sorting Title", example = "By Relevance", requiredMode = Schema.RequiredMode.REQUIRED) - private String title; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java deleted file mode 100644 index 54c9395c5..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilter.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Filter in form of a conjunctive normal form, meaning (A=X OR A=Y) AND (B=M or B=N). " + - "Not selected attributes default to TRUE. Used here to let the backend be a SSOT for the available filter options, " + - "e.g. Transport Mode, Data Model, etc.") -public class CnfFilter { - @Schema(description = "Available attributes to filter by.", requiredMode = Schema.RequiredMode.REQUIRED) - private List fields; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java deleted file mode 100644 index 72687dde4..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterAttribute.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Attribute, e.g. Language") -public class CnfFilterAttribute { - @Schema(description = "Attribute ID", example = "asset:prop:language", requiredMode = Schema.RequiredMode.REQUIRED) - private String id; - @Schema(description = "Attribute Title", example = "Language", requiredMode = Schema.RequiredMode.REQUIRED) - private String title; - @Schema(description = "Available values.", requiredMode = Schema.RequiredMode.REQUIRED) - private List values; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java deleted file mode 100644 index 1b6489382..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterItem.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Attribute Value") -public class CnfFilterItem { - @Schema(description = "Value ID", example = "https://w3id.org/idsa/code/EN", requiredMode = Schema.RequiredMode.REQUIRED) - private String id; - @Schema(description = "Value Title", example = "English", requiredMode = Schema.RequiredMode.REQUIRED) - private String title; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java deleted file mode 100644 index eb73b0449..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValue.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Cnf filter's selected value.") -public class CnfFilterValue { - @Schema(description = "Available attributes to filter by.", requiredMode = Schema.RequiredMode.REQUIRED) - private List selectedAttributeValues; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java deleted file mode 100644 index a0ab4fcd1..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CnfFilterValueAttribute.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Single attribute of selected cnf filter's value") -public class CnfFilterValueAttribute { - @Schema(description = "Attribute ID", example = "asset:prop:language", requiredMode = Schema.RequiredMode.REQUIRED) - private String id; - @Schema(description = "Selected attribute values' IDs.", requiredMode = Schema.RequiredMode.REQUIRED) - private List selectedIds; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java deleted file mode 100644 index a638df809..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorCreationRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Wrapper for adding unknown Connectors with MDS IDs.", requiredMode = Schema.RequiredMode.REQUIRED) -public class ConnectorCreationRequest { - @Schema(description = "Connectors", requiredMode = Schema.RequiredMode.REQUIRED) - private List connectors; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java deleted file mode 100644 index e43d2e273..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageQuery.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Connector Page Detail Query") -public class ConnectorDetailPageQuery { - @Schema(description = "Connector Endpoint1") - private String connectorEndpoint; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java deleted file mode 100644 index 704d8d6cb..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorDetailPageResult.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Connector Detail Page Data") -public class ConnectorDetailPageResult { - @Schema(description = "Connector Participant ID", example = "https://my-test.connector", requiredMode = Schema.RequiredMode.REQUIRED) - private String participantId; - - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) - private String endpoint; - - @Schema(description = "Name of the responsible organization", requiredMode = Schema.RequiredMode.REQUIRED) - private String organizationName; - - @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Last time the connector was successfully refreshed.") - private OffsetDateTime lastSuccessfulRefreshAt; - - @Schema(description = "Last time the connector was tried to be refreshed.") - private OffsetDateTime lastRefreshAttemptAt; - - @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorOnlineStatus onlineStatus; - - @Schema(description = "Number of known data offerings") - private Integer numDataOffers; - - @Schema(description = "Average time to crawl the connector") - private Long connectorCrawlingTimeAvg; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java deleted file mode 100644 index d37dbcc78..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorListEntry.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "A Contract Offer's Connector Status") -public class ConnectorListEntry { - @Schema(description = "Connector Participant ID", example = "my-test-connector", requiredMode = Schema.RequiredMode.REQUIRED) - private String participantId; - - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) - private String endpoint; - - @Schema(description = "Name of the responsible organization", requiredMode = Schema.RequiredMode.REQUIRED) - private String organizationName; - - @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Last time the connector was successfully refreshed.") - private OffsetDateTime lastSuccessfulRefreshAt; - - @Schema(description = "Last time the connector was tried to be refreshed.") - private OffsetDateTime lastRefreshAttemptAt; - - @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorOnlineStatus onlineStatus; - - @Schema(description = "Number of known data offerings") - private Integer numDataOffers; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java deleted file mode 100644 index 78295e455..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorOnlineStatus.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Connector's online status") -public enum ConnectorOnlineStatus { - ONLINE, - OFFLINE, - DEAD -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java deleted file mode 100644 index 38f3afad6..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageQuery.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Filterable Connector Page Query") -public class ConnectorPageQuery { - @Schema(description = "Search query") - private String searchQuery; - - @Schema(description = "Sorting") - private ConnectorPageSortingType sorting; - - @Schema(description = "Page number, one based, meaning the first page is page 1.", example = "1", defaultValue = "1", type = "n") - private Integer pageOneBased; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java deleted file mode 100644 index 247a701c0..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageResult.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Connector Page Data") -public class ConnectorPageResult { - @Schema(description = "Available sorting options", requiredMode = Schema.RequiredMode.REQUIRED) - private List availableSortings; - - @Schema(description = "Pagination Metadata", requiredMode = Schema.RequiredMode.REQUIRED) - private PaginationMetadata paginationMetadata; - - @Schema(description = "Current page of connector list entries", requiredMode = Schema.RequiredMode.REQUIRED) - private List connectors; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java deleted file mode 100644 index 1849de2fb..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingItem.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Available Connector Page Sorting Item") -public class ConnectorPageSortingItem { - @Schema(description = "Sorting ID", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorPageSortingType sorting; - @Schema(description = "Sorting Title", example = "Alphabetically", requiredMode = Schema.RequiredMode.REQUIRED) - private String title; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java deleted file mode 100644 index 0cc284233..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/ConnectorPageSortingType.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -@Schema(description = "Connector List Page's known sorting option IDs") -public enum ConnectorPageSortingType { - ONLINE_STATUS("Online Status"), - MOST_RECENT("Most Recent"), - TITLE("By Title"); - - private final String title; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java deleted file mode 100644 index c5554c765..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailContractOffer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "A contract offer a data offer is available under (as required by the data offer detail page).") -public class DataOfferDetailContractOffer { - @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String contractOfferId; - - @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime updatedAt; - - @Schema(description = "Contract Policy", requiredMode = Schema.RequiredMode.REQUIRED) - private UiPolicy contractPolicy; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java deleted file mode 100644 index 7dc00ff72..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageQuery.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Data Offer Detail Page Query") -public class DataOfferDetailPageQuery { - @Schema(description = "Connector Endpoint") - private String connectorEndpoint; - - @Schema(description = "Asset ID") - private String assetId; -} - diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java deleted file mode 100644 index 354a63c81..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/DataOfferDetailPageResult.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.time.OffsetDateTime; -import java.util.List; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Data Offer Detail Page.") -public class DataOfferDetailPageResult { - @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) - private String assetId; - - @Schema(description = "Connector Endpoint", example = "https://my-test.connector/api/dsp", requiredMode = Schema.RequiredMode.REQUIRED) - private String connectorEndpoint; - - @Schema(description = "Connector Online Status", requiredMode = Schema.RequiredMode.REQUIRED) - private ConnectorOnlineStatus connectorOnlineStatus; - - @Schema(description = "Date to be displayed as last update date, for online connectors it's the last refresh date, for offline connectors it's the creation date or last successful fetch.") - private OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; - - @Schema(description = "Creation date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Update date in Broker", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime updatedAt; - - @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) - private UiAsset asset; - - @Schema(description = "Available Contract Offers", requiredMode = Schema.RequiredMode.REQUIRED) - private List contractOffers; - - @Schema(description = "View Count", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer viewCount; -} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java b/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java deleted file mode 100644 index 4edbebbfe..000000000 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/PaginationMetadata.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.api.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Pagination Metadata") -public class PaginationMetadata { - @Schema(description = "Total number of results", example = "368", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer numTotal; - - @Schema(description = "Visible number of results", example = "20", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer numVisible; - - @Schema(description = "Page number, one based, meaning the first page is page 1.", example = "1", defaultValue = "1", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer pageOneBased; - - @Schema(description = "Items per page", example = "20", type = "n", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer pageSize; -} - diff --git a/extensions/broker-server-api/client-ts/.gitignore b/extensions/broker-server-api/client-ts/.gitignore deleted file mode 100644 index a547bf36d..000000000 --- a/extensions/broker-server-api/client-ts/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/extensions/broker-server-api/client-ts/.prettierignore b/extensions/broker-server-api/client-ts/.prettierignore deleted file mode 100644 index de4d1f007..000000000 --- a/extensions/broker-server-api/client-ts/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/extensions/broker-server-api/client-ts/README.md b/extensions/broker-server-api/client-ts/README.md deleted file mode 100644 index 93c38d954..000000000 --- a/extensions/broker-server-api/client-ts/README.md +++ /dev/null @@ -1,55 +0,0 @@ - -
      -
      - - Logo - - -

      Broker Server API TypeScript Client Library

      - -

      - Report Bug - · - Request Feature -

      -
      - -## About this component - -TypeScript Client Library to access APIs of our Broker Server Backend. - -## How to install - -Requires a NodeJS / NPM project. - -```shell script -npm i --save @sovity.de/broker-server-client -``` - -## How to use - -Configure your Broker Server Client and use endpoints of our Broker Server API: - -```typescript -import { - BrokerServerClient, - buildBrokerServerClient, - CatalogPageResult -} from '@sovity.de/broker-server-client'; - -const brokerServerClient: BrokerServerClient = buildBrokerServerClient({ - managementApiUrl: 'http://localhost:11002/api/management', - managementApiKey: 'ApiKeyDefaultValue', -}); - -let catalog: CatalogPageResult = await edcClient.brokerServerApi.catalogPage(); -``` - -## License - -Apache License 2.0 - see -[LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE) - -## Contact - -sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-api/client-ts/index.html b/extensions/broker-server-api/client-ts/index.html deleted file mode 100644 index f49a7cf04..000000000 --- a/extensions/broker-server-api/client-ts/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Client example - - -
      - - - diff --git a/extensions/broker-server-api/client-ts/package-lock.json b/extensions/broker-server-api/client-ts/package-lock.json deleted file mode 100644 index 0ef861efc..000000000 --- a/extensions/broker-server-api/client-ts/package-lock.json +++ /dev/null @@ -1,3182 +0,0 @@ -{ - "name": "@sovity.de/broker-server-client", - "version": "0.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "@sovity.de/broker-server-client", - "version": "0.0.0", - "license": "Apache-2.0", - "devDependencies": { - "@trivago/prettier-plugin-sort-imports": "^4.1.1", - "@types/node": "^18.15.11", - "prettier": "^2.8.7", - "typescript": "^4.9.3", - "vite": "^4.2.0", - "vite-plugin-dts": "^2.2.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", - "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@microsoft/api-extractor": { - "version": "7.37.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.37.1.tgz", - "integrity": "sha512-wbTL7TZG+9SPvYKwk26390ltoP/uR5621dniqhVp+5OHcn7wIKsT7vX9d/wvdAXD3Ft+7pAiCt6y3dBLFfY/0w==", - "dev": true, - "dependencies": { - "@microsoft/api-extractor-model": "7.28.1", - "@microsoft/tsdoc": "0.14.2", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.60.1", - "@rushstack/rig-package": "0.5.1", - "@rushstack/ts-command-line": "4.16.1", - "colors": "~1.2.1", - "lodash": "~4.17.15", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "source-map": "~0.6.1", - "typescript": "~5.0.4" - }, - "bin": { - "api-extractor": "bin/api-extractor" - } - }, - "node_modules/@microsoft/api-extractor-model": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.1.tgz", - "integrity": "sha512-1hD9gQRu8VR53/e8GI+aql7MtWXHE/XtpOSgphJ6SB7AswqJT0mRZVufUbg3D57UdrchvLKz9b+zqay0Oq2vgg==", - "dev": true, - "dependencies": { - "@microsoft/tsdoc": "0.14.2", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.60.1" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/@microsoft/tsdoc": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", - "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", - "dev": true - }, - "node_modules/@microsoft/tsdoc-config": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", - "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", - "dev": true, - "dependencies": { - "@microsoft/tsdoc": "0.14.2", - "ajv": "~6.12.6", - "jju": "~1.4.0", - "resolve": "~1.19.0" - } - }, - "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "dev": true, - "dependencies": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rushstack/node-core-library": { - "version": "3.60.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.60.1.tgz", - "integrity": "sha512-cWKCImfezPvILKu5eUPkz0Mp/cO/zOSJdPD64KHliBcdmbPHg/sF4rEL7WJkWywXT1RQ/U/N8uKdXMe7jDCXNw==", - "dev": true, - "dependencies": { - "colors": "~1.2.1", - "fs-extra": "~7.0.1", - "import-lazy": "~4.0.0", - "jju": "~1.4.0", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "z-schema": "~5.0.2" - }, - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@rushstack/node-core-library/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@rushstack/rig-package": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", - "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", - "dev": true, - "dependencies": { - "resolve": "~1.22.1", - "strip-json-comments": "~3.1.1" - } - }, - "node_modules/@rushstack/ts-command-line": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.16.1.tgz", - "integrity": "sha512-+OCsD553GYVLEmz12yiFjMOzuPeCiZ3f8wTiFHL30ZVXexTyPmgjwXEhg2K2P0a2lVf+8YBy7WtPoflB2Fp8/A==", - "dev": true, - "dependencies": { - "@types/argparse": "1.0.38", - "argparse": "~1.0.9", - "colors": "~1.2.1", - "string-argv": "~0.3.1" - } - }, - "node_modules/@trivago/prettier-plugin-sort-imports": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", - "integrity": "sha512-iuy2MPVURGdxILTchHr15VAioItuYBejKfcTmQFlxIuqA7jeaT6ngr5aUIG6S6U096d6a6lJCgaOwlRrPLlOPg==", - "dev": true, - "dependencies": { - "@babel/generator": "7.17.7", - "@babel/parser": "^7.20.5", - "@babel/traverse": "7.23.2", - "@babel/types": "7.17.0", - "javascript-natural-sort": "0.7.1", - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@vue/compiler-sfc": "3.x", - "prettier": "2.x - 3.x" - }, - "peerDependenciesMeta": { - "@vue/compiler-sfc": { - "optional": true - } - } - }, - "node_modules/@ts-morph/common": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.18.1.tgz", - "integrity": "sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.12", - "minimatch": "^5.1.0", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@types/argparse": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", - "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true - }, - "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/code-block-writer": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", - "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", - "dev": true - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/colors": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", - "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/javascript-natural-sort": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", - "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", - "dev": true - }, - "node_modules/jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kolorist": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.7.0.tgz", - "integrity": "sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==", - "dev": true - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-morph": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-17.0.1.tgz", - "integrity": "sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g==", - "dev": true, - "dependencies": { - "@ts-morph/common": "~0.18.0", - "code-block-writer": "^11.0.3" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/validator": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-plugin-dts": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.2.0.tgz", - "integrity": "sha512-XmZtv02I7eGWm3DrZbLo1AdJK5gCisk9GqJBpY4N63pDYR6AVUnlyjFP5FCBvSBUfgE0Ppl90bKgtJU9k3AzFw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.15", - "@microsoft/api-extractor": "^7.33.5", - "@rollup/pluginutils": "^5.0.2", - "@rushstack/node-core-library": "^3.53.2", - "debug": "^4.3.4", - "fast-glob": "^3.2.12", - "fs-extra": "^10.1.0", - "kolorist": "^1.6.0", - "magic-string": "^0.29.0", - "ts-morph": "17.0.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": ">=2.9.0" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "dev": true, - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - } - }, - "@babel/generator": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", - "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "dependencies": { - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, - "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "dependencies": { - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "requires": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - } - }, - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "dev": true, - "optional": true - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@microsoft/api-extractor": { - "version": "7.37.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.37.1.tgz", - "integrity": "sha512-wbTL7TZG+9SPvYKwk26390ltoP/uR5621dniqhVp+5OHcn7wIKsT7vX9d/wvdAXD3Ft+7pAiCt6y3dBLFfY/0w==", - "dev": true, - "requires": { - "@microsoft/api-extractor-model": "7.28.1", - "@microsoft/tsdoc": "0.14.2", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.60.1", - "@rushstack/rig-package": "0.5.1", - "@rushstack/ts-command-line": "4.16.1", - "colors": "~1.2.1", - "lodash": "~4.17.15", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "source-map": "~0.6.1", - "typescript": "~5.0.4" - }, - "dependencies": { - "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true - } - } - }, - "@microsoft/api-extractor-model": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.1.tgz", - "integrity": "sha512-1hD9gQRu8VR53/e8GI+aql7MtWXHE/XtpOSgphJ6SB7AswqJT0mRZVufUbg3D57UdrchvLKz9b+zqay0Oq2vgg==", - "dev": true, - "requires": { - "@microsoft/tsdoc": "0.14.2", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.60.1" - } - }, - "@microsoft/tsdoc": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", - "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", - "dev": true - }, - "@microsoft/tsdoc-config": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", - "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", - "dev": true, - "requires": { - "@microsoft/tsdoc": "0.14.2", - "ajv": "~6.12.6", - "jju": "~1.4.0", - "resolve": "~1.19.0" - }, - "dependencies": { - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "dev": true, - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@rushstack/node-core-library": { - "version": "3.60.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.60.1.tgz", - "integrity": "sha512-cWKCImfezPvILKu5eUPkz0Mp/cO/zOSJdPD64KHliBcdmbPHg/sF4rEL7WJkWywXT1RQ/U/N8uKdXMe7jDCXNw==", - "dev": true, - "requires": { - "colors": "~1.2.1", - "fs-extra": "~7.0.1", - "import-lazy": "~4.0.0", - "jju": "~1.4.0", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "z-schema": "~5.0.2" - }, - "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - } - } - }, - "@rushstack/rig-package": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", - "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", - "dev": true, - "requires": { - "resolve": "~1.22.1", - "strip-json-comments": "~3.1.1" - } - }, - "@rushstack/ts-command-line": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.16.1.tgz", - "integrity": "sha512-+OCsD553GYVLEmz12yiFjMOzuPeCiZ3f8wTiFHL30ZVXexTyPmgjwXEhg2K2P0a2lVf+8YBy7WtPoflB2Fp8/A==", - "dev": true, - "requires": { - "@types/argparse": "1.0.38", - "argparse": "~1.0.9", - "colors": "~1.2.1", - "string-argv": "~0.3.1" - } - }, - "@trivago/prettier-plugin-sort-imports": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", - "integrity": "sha512-iuy2MPVURGdxILTchHr15VAioItuYBejKfcTmQFlxIuqA7jeaT6ngr5aUIG6S6U096d6a6lJCgaOwlRrPLlOPg==", - "dev": true, - "requires": { - "@babel/generator": "7.17.7", - "@babel/parser": "^7.20.5", - "@babel/traverse": "7.23.2", - "@babel/types": "7.17.0", - "javascript-natural-sort": "0.7.1", - "lodash": "^4.17.21" - } - }, - "@ts-morph/common": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.18.1.tgz", - "integrity": "sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA==", - "dev": true, - "requires": { - "fast-glob": "^3.2.12", - "minimatch": "^5.1.0", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - } - }, - "@types/argparse": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", - "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true - }, - "@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", - "dev": true - }, - "@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "code-block-writer": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", - "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "colors": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", - "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", - "dev": true - }, - "commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "optional": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "javascript-natural-sort": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", - "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", - "dev": true - }, - "jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "kolorist": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.7.0.tgz", - "integrity": "sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==", - "dev": true - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true - }, - "path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", - "dev": true - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-morph": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-17.0.1.tgz", - "integrity": "sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g==", - "dev": true, - "requires": { - "@ts-morph/common": "~0.18.0", - "code-block-writer": "^11.0.3" - } - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "validator": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", - "dev": true - }, - "vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", - "dev": true, - "requires": { - "esbuild": "^0.18.10", - "fsevents": "~2.3.2", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - } - }, - "vite-plugin-dts": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-2.2.0.tgz", - "integrity": "sha512-XmZtv02I7eGWm3DrZbLo1AdJK5gCisk9GqJBpY4N63pDYR6AVUnlyjFP5FCBvSBUfgE0Ppl90bKgtJU9k3AzFw==", - "dev": true, - "requires": { - "@babel/parser": "^7.20.15", - "@microsoft/api-extractor": "^7.33.5", - "@rollup/pluginutils": "^5.0.2", - "@rushstack/node-core-library": "^3.53.2", - "debug": "^4.3.4", - "fast-glob": "^3.2.12", - "fs-extra": "^10.1.0", - "kolorist": "^1.6.0", - "magic-string": "^0.29.0", - "ts-morph": "17.0.1" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "dev": true, - "requires": { - "commander": "^9.4.1", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - } - } - } -} diff --git a/extensions/broker-server-api/client-ts/package.json b/extensions/broker-server-api/client-ts/package.json deleted file mode 100644 index 101326fbf..000000000 --- a/extensions/broker-server-api/client-ts/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@sovity.de/broker-server-client", - "version": "0.0.0", - "description": "TypeScript API Client for the Broker Server developed by sovity.", - "author": "sovity GmbH", - "license": "Apache-2.0", - "homepage": "https://sovity.de", - "repository": { - "type": "git", - "url": "https://github.com/sovity/edc-broker-server-extension/" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/" - }, - "bugs": { - "url": "https://github.com/sovity/edc-broker-server-extension/issues/new/choose" - }, - "keywords": [ - "sovity", - "api client", - "edc", - "eclipse dataspace components", - "mobility data space", - "Catena-X", - "Mobilithek", - "broker" - ], - "type": "module", - "main": "./dist/sovity-broker-server-client.umd.cjs", - "module": "./dist/sovity-broker-server-client.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/sovity-broker-server-client.js", - "require": "./dist/sovity-broker-server-client.umd.cjs" - } - }, - "files": [ - "dist" - ], - "scripts": { - "dev": "vite", - "build": "npm run format-all && tsc && vite build", - "preview": "vite preview", - "format-all": "prettier --write ." - }, - "devDependencies": { - "@trivago/prettier-plugin-sort-imports": "^4.1.1", - "@types/node": "^18.15.11", - "prettier": "^2.8.7", - "typescript": "^4.9.3", - "vite": "^4.2.0", - "vite-plugin-dts": "^2.2.0" - } -} diff --git a/extensions/broker-server-api/client-ts/prettier.config.cjs b/extensions/broker-server-api/client-ts/prettier.config.cjs deleted file mode 100644 index fbcf1dc96..000000000 --- a/extensions/broker-server-api/client-ts/prettier.config.cjs +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = { - tabWidth: 4, - useTabs: false, - singleQuote: true, - semi: true, - arrowParens: 'always', - trailingComma: 'all', - bracketSameLine: true, - printWidth: 80, - bracketSpacing: false, - proseWrap: 'always', - - // @trivago/prettier-plugin-sort-imports - importOrder: [ - '', - // rest after - '^[./]', - ], - importOrderParserPlugins: [ - 'typescript', - 'classProperties', - 'decorators-legacy', - ], - importOrderSeparation: false, - importOrderSortSpecifiers: true, -}; diff --git a/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts b/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts deleted file mode 100644 index f03bee56b..000000000 --- a/extensions/broker-server-api/client-ts/src/BrokerServerClient.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - BrokerServerApi, - Configuration, - ConfigurationParameters, -} from './generated'; - -/** - * API Client for our sovity Broker Server Client - */ -export interface BrokerServerClient { - brokerServerApi: BrokerServerApi; -} - -/** - * Configure & Build new Broker Server Client - * @param opts opts - */ -export function buildBrokerServerClient( - opts: BrokerServerClientOptions, -): BrokerServerClient { - const config = new Configuration({ - basePath: opts.managementApiUrl, - headers: { - 'x-api-key': opts.managementApiKey ?? 'ApiKeyDefaultValue', - }, - credentials: 'same-origin', - ...opts.configOverrides, - }); - - return { - brokerServerApi: new BrokerServerApi(config), - }; -} - -/** - * Options for instantiating an EDC API Client - */ -export interface BrokerServerClientOptions { - managementApiUrl: string; - managementApiKey?: string; - configOverrides?: Partial; -} diff --git a/extensions/broker-server-api/client-ts/src/generated/.gitignore b/extensions/broker-server-api/client-ts/src/generated/.gitignore deleted file mode 100644 index 654617bb0..000000000 --- a/extensions/broker-server-api/client-ts/src/generated/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -**/* -!.gitignore diff --git a/extensions/broker-server-api/client-ts/src/index.ts b/extensions/broker-server-api/client-ts/src/index.ts deleted file mode 100644 index 861cb71d7..000000000 --- a/extensions/broker-server-api/client-ts/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './BrokerServerClient'; -export * from './generated'; diff --git a/extensions/broker-server-api/client-ts/src/vite-env.d.ts b/extensions/broker-server-api/client-ts/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a..000000000 --- a/extensions/broker-server-api/client-ts/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/extensions/broker-server-api/client-ts/tsconfig.json b/extensions/broker-server-api/client-ts/tsconfig.json deleted file mode 100644 index 138755d01..000000000 --- a/extensions/broker-server-api/client-ts/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "noEmit": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": true, - "skipLibCheck": true - }, - "include": ["src"] -} diff --git a/extensions/broker-server-api/client-ts/vite.config.ts b/extensions/broker-server-api/client-ts/vite.config.ts deleted file mode 100644 index e8143fc31..000000000 --- a/extensions/broker-server-api/client-ts/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -// noinspection JSUnusedGlobalSymbols -import {resolve} from 'path'; -import {defineConfig} from 'vite'; -import dts from 'vite-plugin-dts'; - -// https://vitejs.dev/guide/build.html#library-mode -export default defineConfig({ - build: { - lib: { - entry: resolve(__dirname, 'src/index.ts'), - name: 'sovity-broker-server-client', - fileName: 'sovity-broker-server-client', - }, - sourcemap: true, - }, - plugins: [dts()], -}); diff --git a/extensions/broker-server-api/client/README.md b/extensions/broker-server-api/client/README.md deleted file mode 100644 index 1eed30e4f..000000000 --- a/extensions/broker-server-api/client/README.md +++ /dev/null @@ -1,69 +0,0 @@ - -
      -
      - - Logo - - -

      Broker Server API Java Client Library

      - -

      - Report Bug - · - Request Feature -

      -
      - - -## About this component - -Java API Client Library to be imported and used in arbitrary applications like use-case backends. - -## Installation - -```xml - - - de.sovity.broker - client - ${sovity-edc-broker-server-extension.version} - -``` - -## Usage Example - -```java -import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; -import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; -import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageResult; - -/** - * Example using the Broker Server API Java Client Library - */ -public class BrokerServerClientExample { - - public static final String BROKER_MANAGEMENT_API_URL = "http://localhost:11002/api/v1/management"; - public static final String BROKER_MANAGEMENT_API_KEY = "..."; - - public static void main(String[] args) { - // Configure Client - BrokerServerClient client = BrokerServerClient.builder() - .managementApiUrl(BROKER_MANAGEMENT_API_URL) - .managementApiKey(BROKER_MANAGEMENT_API_KEY) - .build(); - - // EDC API Wrapper APIs are now available for use - CatalogPageQuery catalogPageQuery = new CatalogPageQuery(); - CatalogPageResult catalogPageResult = client.brokerServerApi().catalogPage(catalogPageQuery); - System.out.println(catalogPageResult.getDataOffers()); - } -} -``` - -## License - -Apache License 2.0 - see [LICENSE](../../../LICENSE) - -## Contact - -sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-api/client/build.gradle.kts b/extensions/broker-server-api/client/build.gradle.kts deleted file mode 100644 index efabfe7b2..000000000 --- a/extensions/broker-server-api/client/build.gradle.kts +++ /dev/null @@ -1,137 +0,0 @@ - -plugins { - `java-library` - `maven-publish` - alias(libs.plugins.openapi.generator7) -} - -repositories { - mavenCentral() -} - -// By using a separate configuration we can skip having the Extension Jar in our runtime classpath -val openapiYaml = configurations.create("openapiGenerator") -val buildDir = layout.buildDirectory.get().asFile - -dependencies { - // We only need the openapi.yaml file from this dependency - openapiYaml(project(":extensions:broker-server-api:api")) { - isTransitive = false - } - - // Generated Client's Dependencies - implementation(libs.swagger.annotations) - implementation(libs.findbugs.jsr305) - implementation(libs.okhttp.okhttp) - implementation(libs.okhttp.loggingInterceptor) - implementation(libs.gson) - implementation(libs.gsonFire) - implementation(libs.openapi.jacksonDatabindNullable) - implementation(libs.apache.commonsLang) - implementation(libs.jakarta.annotationApi) - - // Lombok - compileOnly(libs.lombok) - annotationProcessor(libs.lombok) -} - -tasks.getByName("test") { - useJUnitPlatform() -} - -// Extract the openapi file from the JAR -val openapiFileName = "broker-server.yaml" -val targetLocation = buildDir.resolve("openapi") -val extractOpenapiYaml by tasks.registering(Copy::class) { - dependsOn(openapiYaml) - into(targetLocation) - from(zipTree(openapiYaml.singleFile)) { - include(openapiFileName) - } -} - -val openApiGenerate = tasks.getByName("openApiGenerate") { - dependsOn(extractOpenapiYaml) - generatorName.set("java") - configOptions.set( - mutableMapOf( - "invokerPackage" to "de.sovity.edc.ext.brokerserver.client.gen", - "apiPackage" to "de.sovity.edc.ext.brokerserver.client.gen.api", - "modelPackage" to "de.sovity.edc.ext.brokerserver.client.gen.model", - "caseInsensitiveResponseHeaders" to "true", - "additionalModelTypeAnnotations" to listOf( - "@lombok.AllArgsConstructor", - "@lombok.Builder", - "@SuppressWarnings(\"all\")" - ).joinToString("\n"), - "annotationLibrary" to "swagger1", - "hideGenerationTimestamp" to "true", - "useRuntimeException" to "true", - ) - ) - - inputSpec.set(targetLocation.resolve(openapiFileName).path) - outputDir.set("${buildDir}/generated/client-project") -} - -val postprocessGeneratedClient by tasks.registering(Copy::class) { - dependsOn(openApiGenerate) - from("${buildDir}/generated/client-project/src/main/java") - - // @lombok.Builder clashes with the following generated model file. - // It is the base class for OAS3 polymorphism via allOf/anyOf, which we won't use anyway. - exclude("**/AbstractOpenApiSchema.java") - - // The Jax-RS dependency suggested by the generated project was causing issues with quarkus. - // It was again only required for the polymorphism, which we won't use anyway. - filter { if (it == "import javax.ws.rs.core.GenericType;") "" else it } - - into("${buildDir}/generated/sources/openapi/java/main") -} -sourceSets["main"].java.srcDir("${buildDir}/generated/sources/openapi/java/main") - -checkstyle { - // Checkstyle loathes the generated files - // TODO make checkstyle skip generated files only - this.sourceSets = emptyList() -} - - -tasks.getByName("compileJava") { - dependsOn(postprocessGeneratedClient) - options.compilerArgs = listOf("-Xlint:none") -} - -val sourcesJar = tasks.getByName("sourcesJar") { - dependsOn(postprocessGeneratedClient) -} - -val javadocJar = tasks.getByName("javadocJar") { - dependsOn(postprocessGeneratedClient) -} - -artifacts { - add("archives", sourcesJar) - add("archives", javadocJar) -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -tasks.withType { - val fullOptions = this.options as StandardJavadocDocletOptions - fullOptions.tags = listOf("http.response.details:a:Http Response Details") - fullOptions.addStringOption("Xdoclint:none", "-quiet") -} - -group = libs.versions.sovityBrokerServerGroup.get() - -publishing { - publications { - create(project.name) { - from(components["java"]) - } - } -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java deleted file mode 100644 index fcfec3d17..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClient.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.client; - -import de.sovity.edc.ext.brokerserver.client.gen.api.BrokerServerApi; -import lombok.Value; -import lombok.experimental.Accessors; - -/** - * API Client for the Broker Server. - */ -@Value -@Accessors(fluent = true) -public class BrokerServerClient { - BrokerServerApi brokerServerApi; - - public static BrokerServerClientBuilder builder() { - return new BrokerServerClientBuilder(); - } -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java deleted file mode 100644 index ce5d34e7b..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientBuilder.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.client; - -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - -@Getter -@Setter -@Accessors(fluent = true, chain = true) -public class BrokerServerClientBuilder { - /** - * Management API Base URL, e.g. https://my-broker.com/backend/management - */ - private String managementApiUrl; - - /** - * Enables EDC Management API Key authentication. - */ - private String managementApiKey = "ApiKeyDefaultValue"; - - public BrokerServerClient build() { - return BrokerServerClientFactory.newClient(this); - } -} diff --git a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java b/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java deleted file mode 100644 index 01bb388a4..000000000 --- a/extensions/broker-server-api/client/src/main/java/de/sovity/edc/ext/brokerserver/client/BrokerServerClientFactory.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.client; - -import de.sovity.edc.ext.brokerserver.client.gen.ApiClient; -import de.sovity.edc.ext.brokerserver.client.gen.api.BrokerServerApi; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; - -/** - * Builds {@link BrokerServerClient}s. - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class BrokerServerClientFactory { - - public static BrokerServerClient newClient(BrokerServerClientBuilder builder) { - var apiClient = new ApiClient() - .setServerIndex(null) - .setBasePath(builder.managementApiUrl()); - - if (StringUtils.isNotBlank(builder.managementApiKey())) { - apiClient.addDefaultHeader("x-api-key", builder.managementApiKey()); - } - - return new BrokerServerClient( - new BrokerServerApi(apiClient) - ); - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java deleted file mode 100644 index 3dd3c95e3..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactoryHijacker.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.sovity.edc.ext.brokerserver.db; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.jooq.DSLContext; - -/** - * Hijack all {@link DslContextFactory}s from test code (single thread only) - */ -@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) -public class DslContextFactoryHijacker { - @Getter - private static DSLContext parentDslContext = null; - - /** - * Our tests currently have no access to the running extension's context, save for REST calls. - *
      - * We use this class to hack all DslContextFactories via {@link #parentDslContext}. - *
      - * If we set the {@link #parentDslContext} to one we created with a transaction we won't commit, we won't have to reset the DB between tests. - * - * @param testTransactionDslContext parent dsl context containing the parent transaction - * @param r code to run - */ - public static void withParentDslContext(DSLContext testTransactionDslContext, Runnable r) { - if (parentDslContext != null) { - throw new IllegalStateException("Tests are being run in parallel, which won't work with our current architecture."); - } - - DslContextFactoryHijacker.parentDslContext = testTransactionDslContext; - - try { - r.run(); - } finally { - DslContextFactoryHijacker.parentDslContext = null; - } - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java deleted file mode 100644 index bee8c7b36..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.system.configuration.Config; -import org.flywaydb.core.Flyway; - -import javax.sql.DataSource; - -import static de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE; - -/** - * Quickly launch {@link Flyway} from EDC Config - */ -@RequiredArgsConstructor -public class FlywayFactory { - private final Config config; - - /** - * Configure and launch {@link Flyway}. - * - * @param dataSource data source - * @return {@link Flyway} - */ - public Flyway setupFlyway(DataSource dataSource) { - return Flyway.configure() - .baselineOnMigrate(true) - .dataSource(dataSource) - .cleanDisabled(!config.getBoolean(FLYWAY_CLEAN_ENABLE, false)) - .table("flyway_schema_history") - .locations("classpath:db/migration") - .mixed(true) - .load(); - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java deleted file mode 100644 index 92111349f..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/FlywayMigrator.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.persistence.EdcPersistenceException; -import org.eclipse.edc.spi.system.configuration.Config; -import org.flywaydb.core.Flyway; -import org.flywaydb.core.api.FlywayException; - - -/** - * Custom flyway migration logic and logging. - */ -@RequiredArgsConstructor -public class FlywayMigrator { - private final Flyway flyway; - private final Config config; - private final Monitor monitor; - - /** - * Run migrations and potentially run flyway repair - */ - public void migrateAndRepair() { - if (config.getBoolean(PostgresFlywayExtension.FLYWAY_CLEAN, false)) { - monitor.info("Cleaning database before migrations, since %s=true and %s=true.".formatted( - PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, PostgresFlywayExtension.FLYWAY_CLEAN - )); - flyway.clean(); - } - try { - migrate(); - } catch (FlywayException e) { - if (isFlywayRepairEnabled()) { - try { - repair(); - migrate(); - } catch (FlywayException e2) { - throw migrationFailed(e2); - } - } else { - throw migrationFailed(e); - } - } - } - - private void migrate() { - var migrateResult = flyway.migrate(); - if (migrateResult.migrationsExecuted > 0) { - monitor.info(String.format( - "Successfully migrated database from version %s to version %s", - migrateResult.initialSchemaVersion, - migrateResult.targetSchemaVersion - )); - } else { - monitor.info(String.format( - "No migration necessary. Current version is %s", - migrateResult.initialSchemaVersion - )); - } - } - - private void repair() { - var repairResult = flyway.repair(); - if (!repairResult.repairActions.isEmpty()) { - var repairActions = String.join(", ", repairResult.repairActions); - monitor.info(String.format("Flyway Repair actions: %s", repairActions)); - } - - if (!repairResult.warnings.isEmpty()) { - var warnings = String.join(", ", repairResult.warnings); - throw new EdcPersistenceException(String.format("Flyway Repair failed: %s", warnings)); - } - } - - private boolean isFlywayRepairEnabled() { - return config.getBoolean(PostgresFlywayExtension.FLYWAY_REPAIR, true); - } - - - private EdcPersistenceException migrationFailed(FlywayException e) { - return new EdcPersistenceException("Flyway migration failed", e); - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java deleted file mode 100644 index c887c9a1b..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/PostgresFlywayExtension.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import com.zaxxer.hikari.HikariDataSource; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; - -public class PostgresFlywayExtension implements ServiceExtension { - @Setting(required = true) - public static final String JDBC_URL = "edc.datasource.default.url"; - @Setting(required = true) - public static final String JDBC_USER = "edc.datasource.default.user"; - @Setting(required = true) - public static final String JDBC_PASSWORD = "edc.datasource.default.password"; - @Setting - public static final String FLYWAY_REPAIR = "edc.flyway.repair"; - @Setting - public static final String FLYWAY_CLEAN_ENABLE = "edc.flyway.clean.enable"; - @Setting - public static final String FLYWAY_CLEAN = "edc.flyway.clean"; - @Setting - public static final String DB_CONNECTION_POOL_SIZE = "edc.broker.server.db.connection.pool.size"; - @Setting - public static final String DB_CONNECTION_TIMEOUT_IN_MS = "edc.broker.server.db.connection.timeout.in.ms"; - - private HikariDataSource dataSource; - - @Override - public String name() { - return "Postgres Flyway Extension (Broker Server)"; - } - - @Override - public void initialize(ServiceExtensionContext context) { - var config = context.getConfig(); - var monitor = context.getMonitor(); - - var dataSourceFactory = new DataSourceFactory(config); - dataSource = dataSourceFactory.newDataSource(); - - var flywayFactory = new FlywayFactory(config); - var flyway = flywayFactory.setupFlyway(dataSource); - var flywayMigrator = new FlywayMigrator(flyway, config, monitor); - flywayMigrator.migrateAndRepair(); - } - - @Override - public void shutdown() { - dataSource.close(); - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java deleted file mode 100644 index 0efef4996..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/ConfigUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db.utils; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.Validate; -import org.eclipse.edc.spi.system.configuration.Config; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class ConfigUtils { - - public static String getRequiredStringProperty(Config config, String name) { - String value = config.getString(name, ""); - Validate.notBlank(value, "EDC Property '%s' is required".formatted(name)); - return value; - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java b/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java deleted file mode 100644 index e6d664ce2..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/utils/JdbcCredentials.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db.utils; - -import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; -import org.eclipse.edc.spi.system.configuration.Config; - -/** - * JDBC Credentials - * - * @param jdbcUrl JDBC URL without credentials - * @param jdbcUser JDBC User - * @param jdbcPassword JDBC Password - */ -public record JdbcCredentials( - String jdbcUrl, - String jdbcUser, - String jdbcPassword -) { - public static JdbcCredentials fromConfig(Config config) { - return new JdbcCredentials( - ConfigUtils.getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_URL), - ConfigUtils.getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_USER), - ConfigUtils.getRequiredStringProperty(config, PostgresFlywayExtension.JDBC_PASSWORD) - ); - } -} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index 1f8b84893..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1 +0,0 @@ -de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension \ No newline at end of file diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql deleted file mode 100644 index 9c5ff65f3..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V1__MS8.sql +++ /dev/null @@ -1,529 +0,0 @@ --- --- Copyright (c) 2022 Daimler TSS GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- Daimler TSS GmbH - Initial SQL Query --- - --- THIS SCHEMA HAS BEEN WRITTEN AND TESTED ONLY FOR POSTGRES - --- table: edc_asset -CREATE TABLE IF NOT EXISTS edc_asset -( - asset_id VARCHAR NOT NULL, - PRIMARY KEY (asset_id) -); - --- table: edc_asset_dataaddress -CREATE TABLE IF NOT EXISTS edc_asset_dataaddress -( - asset_id_fk VARCHAR NOT NULL, - properties JSON NOT NULL, - PRIMARY KEY (asset_id_fk), - FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE -); -COMMENT ON COLUMN edc_asset_dataaddress.properties IS 'DataAddress properties serialized as JSON'; - --- table: edc_asset_property -CREATE TABLE IF NOT EXISTS edc_asset_property -( - asset_id_fk VARCHAR NOT NULL, - property_name VARCHAR NOT NULL, - property_value VARCHAR NOT NULL, - property_type VARCHAR NOT NULL, - PRIMARY KEY (asset_id_fk, property_name), - FOREIGN KEY (asset_id_fk) REFERENCES edc_asset (asset_id) ON DELETE CASCADE -); - -COMMENT ON COLUMN edc_asset_property.property_name IS - 'Asset property key'; -COMMENT ON COLUMN edc_asset_property.property_value IS - 'Asset property value'; -COMMENT ON COLUMN edc_asset_property.property_type IS - 'Asset property class name'; - -CREATE INDEX IF NOT EXISTS idx_edc_asset_property_value - ON edc_asset_property (property_name, property_value); - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- - -ALTER TABLE edc_asset - ADD created_at BIGINT; - -UPDATE edc_asset -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; - -ALTER TABLE edc_asset - ALTER COLUMN created_at SET NOT NULL; - --- --- Copyright (c) 2022 Daimler TSS GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- Daimler TSS GmbH - Initial SQL Query --- Microsoft Corporation - refactoring --- - --- table: edc_contract_definitions --- only intended for and tested with H2 and Postgres! -CREATE TABLE IF NOT EXISTS edc_contract_definitions -( - contract_definition_id VARCHAR NOT NULL, - access_policy_id VARCHAR NOT NULL, - contract_policy_id VARCHAR NOT NULL, - selector_expression JSON NOT NULL, - PRIMARY KEY (contract_definition_id) -); - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- -ALTER TABLE edc_contract_definitions - ADD created_at BIGINT; - -UPDATE edc_contract_definitions -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; - -ALTER TABLE edc_contract_definitions - ALTER COLUMN created_at SET NOT NULL; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- - -ALTER TABLE edc_contract_definitions - ADD validity BIGINT; --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- -UPDATE edc_contract_definitions -SET validity=60 * 60 * 24 * 365 -WHERE validity IS NULL; - --- Statements are designed for and tested with Postgres only! - -CREATE TABLE IF NOT EXISTS edc_lease -( - leased_by VARCHAR NOT NULL, - leased_at BIGINT, - lease_duration INTEGER DEFAULT 60000 NOT NULL, - lease_id VARCHAR NOT NULL - CONSTRAINT lease_pk - PRIMARY KEY -); - -COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; - -COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; - - -CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex - ON edc_lease (lease_id); - - - -CREATE TABLE IF NOT EXISTS edc_contract_agreement -( - agr_id VARCHAR NOT NULL - CONSTRAINT contract_agreement_pk - PRIMARY KEY, - provider_agent_id VARCHAR, - consumer_agent_id VARCHAR, - signing_date BIGINT, - start_date BIGINT, - end_date INTEGER, - asset_id VARCHAR NOT NULL, - policy JSON -); - - -CREATE TABLE IF NOT EXISTS edc_contract_negotiation -( - id VARCHAR NOT NULL - CONSTRAINT contract_negotiation_pk - PRIMARY KEY, - correlation_id VARCHAR, - counterparty_id VARCHAR NOT NULL, - counterparty_address VARCHAR NOT NULL, - protocol VARCHAR DEFAULT 'ids-multipart'::CHARACTER VARYING NOT NULL, - type INTEGER DEFAULT 0 NOT NULL, - state INTEGER DEFAULT 0 NOT NULL, - state_count INTEGER DEFAULT 0, - state_timestamp BIGINT, - error_detail VARCHAR, - agreement_id VARCHAR - CONSTRAINT contract_negotiation_contract_agreement_id_fk - REFERENCES edc_contract_agreement, - contract_offers JSON, - trace_context JSON, - lease_id VARCHAR - CONSTRAINT contract_negotiation_lease_lease_id_fk - REFERENCES edc_lease - ON DELETE SET NULL, - CONSTRAINT provider_correlation_id CHECK (type = '0' OR correlation_id IS NOT NULL) -); - -COMMENT ON COLUMN edc_contract_negotiation.agreement_id IS 'ContractAgreement serialized as JSON'; - -COMMENT ON COLUMN edc_contract_negotiation.contract_offers IS 'List serialized as JSON'; - -COMMENT ON COLUMN edc_contract_negotiation.trace_context IS 'Map serialized as JSON'; - - -CREATE INDEX IF NOT EXISTS contract_negotiation_correlationid_index - ON edc_contract_negotiation (correlation_id); - -CREATE UNIQUE INDEX IF NOT EXISTS contract_negotiation_id_uindex - ON edc_contract_negotiation (id); - -CREATE UNIQUE INDEX IF NOT EXISTS contract_agreement_id_uindex - ON edc_contract_agreement (agr_id); - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- -ALTER TABLE edc_contract_negotiation - ADD created_at BIGINT, - ADD updated_at BIGINT; - -UPDATE edc_contract_negotiation -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; -UPDATE edc_contract_negotiation -SET updated_at=created_at; - -ALTER TABLE edc_contract_negotiation - ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE edc_contract_negotiation - ALTER COLUMN updated_at SET NOT NULL; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- - -UPDATE edc_contract_negotiation -SET contract_offers = co.contract_offers_edited -FROM (SELECT cn.id, - jsonb_agg( - jsonb_set( - jsonb_set( - elems, - '{contractStart}', - to_json(to_char(to_timestamp(created_at / 1000) AT TIME ZONE 'UTC', - 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb - ), - '{contractEnd}', - to_json(to_char(to_timestamp((created_at / 1000) + 60 * 60 * 24 * 365) AT TIME ZONE 'UTC', - 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')::text)::jsonb - ) - )::json as contract_offers_edited - FROM edc_contract_negotiation cn, - jsonb_array_elements(cn.contract_offers::jsonb) elems - GROUP BY cn.id) co -WHERE edc_contract_negotiation.id = co.id; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - initial API and implementation for DataplaneInstances --- --- -CREATE TABLE IF NOT EXISTS edc_data_plane_instance -( - id VARCHAR NOT NULL, - data JSON NOT NULL, - PRIMARY KEY (id) -); - --- --- Copyright (c) 2022 ZF Friedrichshafen AG --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- ZF Friedrichshafen AG - Initial SQL Query --- - --- Statements are designed for and tested with Postgres only! - --- table: edc_policydefinitions -CREATE TABLE IF NOT EXISTS edc_policydefinitions -( - policy_id VARCHAR NOT NULL, - permissions JSON, - prohibitions JSON, - duties JSON, - extensible_properties JSON, - inherits_from VARCHAR, - assigner VARCHAR, - assignee VARCHAR, - target VARCHAR, - policy_type VARCHAR NOT NULL, - PRIMARY KEY (policy_id) -); - -COMMENT ON COLUMN edc_policydefinitions.permissions IS 'Java List serialized as JSON'; -COMMENT ON COLUMN edc_policydefinitions.prohibitions IS 'Java List serialized as JSON'; -COMMENT ON COLUMN edc_policydefinitions.duties IS 'Java List serialized as JSON'; -COMMENT ON COLUMN edc_policydefinitions.extensible_properties IS 'Java Map serialized as JSON'; -COMMENT ON COLUMN edc_policydefinitions.policy_type IS 'Java PolicyType serialized as JSON'; - -CREATE UNIQUE INDEX IF NOT EXISTS edc_policydefinitions_id_uindex - ON edc_policydefinitions (policy_id); - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- -ALTER TABLE edc_policydefinitions - ADD created_at BIGINT; - -UPDATE edc_policydefinitions -SET created_at=EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000; - -ALTER TABLE edc_policydefinitions - ALTER COLUMN created_at SET NOT NULL; - --- Statements are designed for and tested with Postgres only! - -CREATE TABLE IF NOT EXISTS edc_lease -( - leased_by VARCHAR NOT NULL, - leased_at BIGINT, - lease_duration INTEGER NOT NULL, - lease_id VARCHAR NOT NULL - CONSTRAINT lease_pk - PRIMARY KEY -); - -COMMENT ON COLUMN edc_lease.leased_at IS 'posix timestamp of lease'; - -COMMENT ON COLUMN edc_lease.lease_duration IS 'duration of lease in milliseconds'; - -CREATE TABLE IF NOT EXISTS edc_transfer_process -( - transferprocess_id VARCHAR NOT NULL - CONSTRAINT transfer_process_pk - PRIMARY KEY, - type VARCHAR NOT NULL, - state INTEGER NOT NULL, - state_count INTEGER DEFAULT 0 NOT NULL, - state_time_stamp BIGINT, - created_time_stamp BIGINT, - trace_context JSON, - error_detail VARCHAR, - resource_manifest JSON, - provisioned_resource_set JSON, - content_data_address JSON, - deprovisioned_resources JSON, - lease_id VARCHAR - CONSTRAINT transfer_process_lease_lease_id_fk - REFERENCES edc_lease - ON DELETE SET NULL -); - -COMMENT ON COLUMN edc_transfer_process.trace_context IS 'Java Map serialized as JSON'; - -COMMENT ON COLUMN edc_transfer_process.resource_manifest IS 'java ResourceManifest serialized as JSON'; - -COMMENT ON COLUMN edc_transfer_process.provisioned_resource_set IS 'ProvisionedResourceSet serialized as JSON'; - -COMMENT ON COLUMN edc_transfer_process.content_data_address IS 'DataAddress serialized as JSON'; - -COMMENT ON COLUMN edc_transfer_process.deprovisioned_resources IS 'List of deprovisioned resources, serialized as JSON'; - - -CREATE UNIQUE INDEX IF NOT EXISTS transfer_process_id_uindex - ON edc_transfer_process (transferprocess_id); - -CREATE TABLE IF NOT EXISTS edc_data_request -( - datarequest_id VARCHAR NOT NULL - CONSTRAINT data_request_pk - PRIMARY KEY, - process_id VARCHAR NOT NULL, - connector_address VARCHAR NOT NULL, - protocol VARCHAR NOT NULL, - connector_id VARCHAR, - asset_id VARCHAR NOT NULL, - contract_id VARCHAR NOT NULL, - data_destination JSON NOT NULL, - managed_resources BOOLEAN DEFAULT TRUE, - properties JSON, - transfer_type JSON, - transfer_process_id VARCHAR NOT NULL - CONSTRAINT data_request_transfer_process_id_fk - REFERENCES edc_transfer_process - ON UPDATE RESTRICT ON DELETE CASCADE -); - -COMMENT ON COLUMN edc_data_request.data_destination IS 'DataAddress serialized as JSON'; - -COMMENT ON COLUMN edc_data_request.properties IS 'java Map serialized as JSON'; - -COMMENT ON COLUMN edc_data_request.transfer_type IS 'TransferType serialized as JSON'; - - -CREATE UNIQUE INDEX IF NOT EXISTS data_request_id_uindex - ON edc_data_request (datarequest_id); - -CREATE UNIQUE INDEX IF NOT EXISTS lease_lease_id_uindex - ON edc_lease (lease_id); - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-7 EDC --- --- - -ALTER TABLE edc_transfer_process - RENAME COLUMN created_time_stamp TO created_at; - -UPDATE edc_transfer_process -SET created_at = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 -WHERE created_at = NULL; - -ALTER TABLE edc_transfer_process - ADD updated_at BIGINT; - -UPDATE edc_transfer_process -SET updated_at=created_at; - -ALTER TABLE edc_transfer_process - ALTER COLUMN updated_at SET NOT NULL; -ALTER TABLE edc_transfer_process - ALTER COLUMN created_at SET NOT NULL; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- -ALTER TABLE edc_transfer_process - ADD transferprocess_properties JSON; - --- --- Copyright (c) 2023 sovity GmbH --- --- This program and the accompanying materials are made available under the --- terms of the Apache License, Version 2.0 which is available at --- https://www.apache.org/licenses/LICENSE-2.0 --- --- SPDX-License-Identifier: Apache-2.0 --- --- Contributors: --- sovity GmbH - Update Tables to Milestone-8 EDC --- --- -UPDATE edc_transfer_process -SET transferprocess_properties = '{}'::json -WHERE transferprocess_properties IS NULL; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql deleted file mode 100644 index 45481e629..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V2__PoC.sql +++ /dev/null @@ -1,94 +0,0 @@ -create type connector_online_status as enum ('ONLINE', 'OFFLINE'); -create type measurement_type as enum ('CONNECTOR_REFRESH'); -create type measurement_error_status as enum ('ERROR', 'OK'); - -create table connector -( - endpoint text not null, - connector_id text not null, - created_at timestamp with time zone not null, - last_refresh_attempt_at timestamp with time zone, - last_successful_refresh_at timestamp with time zone, - online_status connector_online_status not null, - - PRIMARY KEY (endpoint) -); - -create table data_offer -( - connector_endpoint text not null, - asset_id text not null, - asset_properties jsonb not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone, - - PRIMARY KEY (connector_endpoint, asset_id), - FOREIGN KEY (connector_endpoint) REFERENCES connector (endpoint) -); - -create table data_offer_contract_offer -( - contract_offer_id text not null, - connector_endpoint text not null, - asset_id text not null, - policy jsonb not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone, - - PRIMARY KEY (contract_offer_id), - FOREIGN KEY (connector_endpoint, asset_id) REFERENCES data_offer (connector_endpoint, asset_id), - FOREIGN KEY (connector_endpoint) REFERENCES connector (endpoint) -); - -create type broker_event_type as enum ( - --Connector was successfully updated, and changes were incorporated - 'CONNECTOR_UPDATED', - - --Connector went online - 'CONNECTOR_STATUS_CHANGE_ONLINE', - - --Connector went offline - 'CONNECTOR_STATUS_CHANGE_OFFLINE', - - --Connector was "force deleted" - 'CONNECTOR_STATUS_CHANGE_FORCE_DELETED', - - --Contract Offer was updated - 'CONTRACT_OFFER_UPDATED', - - --Contract Offer was clicked - 'CONTRACT_OFFER_CLICK' -); - -create type broker_event_status as enum ( - -- Default - 'OK', - - -- Failures - 'ERROR' -); - -create table broker_event_log -( - id serial primary key, - created_at timestamp with time zone not null, - user_message text not null, - event broker_event_type not null, - event_status broker_event_status not null, - connector_endpoint text, - asset_id text, - error_stack text, - duration_in_ms bigint -); - -create table broker_execution_time_measurement -( - id serial primary key, - created_at timestamp with time zone not null, - connector_endpoint text not null, - duration_in_ms bigint not null, - type measurement_type not null, - error_status measurement_error_status not null -); - -create index speedup on broker_event_log (connector_endpoint, asset_id, event_status); diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql deleted file mode 100644 index ecffc4e51..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_1__MvP.sql +++ /dev/null @@ -1,36 +0,0 @@ -create collation if not exists alphanumeric_with_natural_sort (provider = icu, locale = 'en-u-kn-true'); - -create type connector_data_offers_exceeded as enum ('OK', 'EXCEEDED'); -create type connector_contract_offers_exceeded as enum ('OK', 'EXCEEDED'); - -alter table broker_event_log - drop column duration_in_ms; - -alter table connector - alter column endpoint type text collate alphanumeric_with_natural_sort, - add column data_offers_exceeded connector_data_offers_exceeded, - add column contract_offers_exceeded connector_contract_offers_exceeded; - -update connector -set data_offers_exceeded = 'OK', - contract_offers_exceeded = 'OK'; - -alter table connector - alter column data_offers_exceeded set not null, - alter column contract_offers_exceeded set not null; - -alter table data_offer - alter column asset_id type text collate alphanumeric_with_natural_sort, - add column asset_name text collate alphanumeric_with_natural_sort; - -update data_offer -set asset_name = coalesce(asset_properties ->> 'asset:prop:name', asset_id); - -alter table data_offer - alter column asset_name set not null; - --- update contract offer table's primary key -alter table data_offer_contract_offer - drop constraint data_offer_contract_offer_pkey; -alter table data_offer_contract_offer - add primary key (connector_endpoint, asset_id, contract_offer_id); diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql deleted file mode 100644 index c39525a59..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V3_2__MvP_Non_Transactional.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Changes to Enums are non-transactional and must be supplied in a separate migration script for flyway - --- Connector Data Offer Limit was exceeded -alter type broker_event_type add value 'CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED'; --- Connector Data Offer Limit was not exceeded -alter type broker_event_type add value 'CONNECTOR_DATA_OFFER_LIMIT_OK'; --- Connector Contract Offer Limit was exceeded -alter type broker_event_type add value 'CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED'; --- Connector Contract Offer Limit was not exceeded -alter type broker_event_type add value 'CONNECTOR_CONTRACT_OFFER_LIMIT_OK'; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql deleted file mode 100644 index da600f9e5..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_1__MvP_Bugfixes_1_1_0.sql +++ /dev/null @@ -1,9 +0,0 @@ -create table data_offer_view_count ( - id serial primary key, - connector_endpoint text not null, - asset_id text not null, - date timestamp with time zone not null -); - -create index data_offer_view_count_speedup on data_offer_view_count (connector_endpoint, asset_id); - diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql deleted file mode 100644 index 986e58cfd..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V4_2__MvP_Bugfixes_Non_Transactional.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Changes to Enums are non-transactional and must be supplied in a separate migration script for flyway - --- Connector deleted due to being offline for too long -alter type broker_event_type add value 'CONNECTOR_KILLED_DUE_TO_OFFLINE_FOR_TOO_LONG'; -alter type connector_online_status add value 'DEAD'; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql deleted file mode 100644 index fea8c8173..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V5_1__MvP_Fix_Asset_JSON_Properties.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Maps JSON Asset Properties to String --- '{"a": "b", "c": [1, 2], "d": true}'::jsonb becomes '{"a": "b", "c": "[1, 2]", "d": "true"}'::jsonb -create -or replace function pg_temp.migrate_asset_properties(asset_properties jsonb) returns jsonb as -$$ -begin -return (select jsonb_object_agg(key, case when jsonb_typeof(value) = 'string' then value #>> '{}' else value::text end) - from jsonb_each(asset_properties)); -end; -$$ -language plpgsql; - --- Fix existing data offer asssets -update data_offer -set asset_properties = pg_temp.migrate_asset_properties(asset_properties); - --- Add new Event Log Status -alter type broker_event_type add value 'CONNECTOR_DELETED'; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql deleted file mode 100644 index c8edbe6fc..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V6__EDC0.sql +++ /dev/null @@ -1,166 +0,0 @@ --- Migration Script for Broker from MS8 to EDC 0 - --- Migrates an Asset ID -create - or replace function pg_temp.migrate_asset_id(asset_id text) returns text as -$$ -begin - return replace(replace(asset_id::text, 'urn:artifact:', ''), ':', '-'); -end; -$$ - language plpgsql; - --- Migrates a Connector Endpoint to EDC 0 -create - or replace function pg_temp.migrate_connector_endpoint(endpoint text) returns text as -$$ -begin - return pg_temp.replace_suffix(endpoint, '/api/v1/ids/data', '/api/dsp'); -end; -$$ - language plpgsql; - --- Creates a valid Asset JSON-LD from an Asset ID and Asset Title -create - or replace function pg_temp.build_asset_json_ld(asset_id text, asset_title text) returns jsonb as -$$ -begin - return jsonb_build_object( - '@id', asset_id, - 'https://w3id.org/edc/v0.0.1/ns/properties', jsonb_build_object( - 'https://w3id.org/edc/v0.0.1/ns/id', asset_id, - 'http://purl.org/dc/terms/title', asset_title - ) - ); -end; -$$ - language plpgsql; - --- Utility Function: replaceSuffix -create - or replace function pg_temp.replace_suffix(str text, old_suffix text, new_suffix text) returns text as -$$ -begin - return case - when pg_temp.ends_with(str, old_suffix) then - left(str, length(str) - length(old_suffix)) || new_suffix - else - str - end; -end; -$$ - language plpgsql; - --- Utility Function: endsWith -create or replace function pg_temp.ends_with(str text, suffix text) - returns boolean as -$$ -begin - return right(str, length(suffix)) = suffix; -end; -$$ language plpgsql; - --- Utility Function: Drops fkey constraints that have auto-generated names. Different Postgresql versions generated different names. -create or replace function pg_temp.drop_constraints_containing_fkey(table_name text) - returns void as -$$ -declare - i record; -begin - for i in (select conname - from pg_catalog.pg_constraint con - inner join pg_catalog.pg_class rel on rel.oid = con.conrelid - inner join pg_catalog.pg_namespace nsp on nsp.oid = connamespace - where rel.relname = table_name - and conname like '%fkey%') - loop - execute format('alter table %s drop constraint %s', table_name, i.conname); - end loop; -end; -$$ language plpgsql; - - --- Remove Connector Tables --- All connector tables should be empty --- There should be no references from broker tables to connector tables -drop table edc_asset cascade; -drop table edc_asset_dataaddress cascade; -drop table edc_asset_property cascade; -drop table edc_contract_agreement cascade; -drop table edc_contract_definitions cascade; -drop table edc_contract_negotiation cascade; -drop table edc_data_plane_instance cascade; -drop table edc_data_request cascade; -drop table edc_lease cascade; -drop table edc_policydefinitions cascade; -drop table edc_transfer_process cascade; - - --- Drop constraints -select pg_temp.drop_constraints_containing_fkey('data_offer'); -select pg_temp.drop_constraints_containing_fkey('data_offer_contract_offer'); - --- Migrate Connector Endpoints -update broker_event_log -set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); -update broker_execution_time_measurement -set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); -update connector -set endpoint = pg_temp.migrate_connector_endpoint(endpoint); -update data_offer -set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); -update data_offer_contract_offer -set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); -update data_offer_view_count -set connector_endpoint = pg_temp.migrate_connector_endpoint(connector_endpoint); - - --- Migrate Asset IDs -update broker_event_log -set asset_id = pg_temp.migrate_asset_id(asset_id); -update data_offer -set asset_id = pg_temp.migrate_asset_id(asset_id); -update data_offer_contract_offer -set asset_id = pg_temp.migrate_asset_id(asset_id); -update data_offer_view_count -set asset_id = pg_temp.migrate_asset_id(asset_id); - --- Rename data_offer_contract_offer to contract_offer -alter table data_offer_contract_offer - rename to contract_offer; - --- Rename Connector ID to Participant ID -alter table connector - rename column connector_id to participant_id; - --- Add constraints -alter table data_offer - add constraint data_offer_connector_endpoint_fkey - foreign key (connector_endpoint) references connector (endpoint); -alter table contract_offer - add constraint contract_offer_data_offer_fkey - foreign key (connector_endpoint, asset_id) references data_offer (connector_endpoint, asset_id); -alter table contract_offer - add constraint contract_offer_connector_fkey - foreign key (connector_endpoint) references connector (endpoint); - --- Migrate to Asset JSON-LD -alter table data_offer - rename column asset_properties to asset_json_ld; -alter table data_offer - rename column asset_name to asset_title; -update data_offer -set asset_json_ld = pg_temp.build_asset_json_ld(asset_id, asset_title); - --- Extracted Asset Metadata from the JSON-LD for Search / Filtering -alter table data_offer - add column description text not null default '', - add column curator_organization_name text not null default '', - add column data_category text not null default '', - add column data_subcategory text not null default '', - add column data_model text not null default '', - add column transport_mode text not null default '', - add column geo_reference_method text not null default '', - add column keywords text[] not null default '{}', - -- comma joined keywords for easier search - add column keywords_comma_joined text not null default ''; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql deleted file mode 100644 index d3e19bca0..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/migration/V7__Organization_Metadata.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Create table for organization metadata -create table organization_metadata -( - mds_id text not null primary key, - name text not null -); - --- Add MDS-ID column to organization table -alter table connector add column mds_id text; -update connector set mds_id = split_part(participant_id, '.', 1) -where participant_id ~ '^MDSL[A-Za-z0-9]+\.C[A-Za-z0-9]+$'; diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql b/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql deleted file mode 100644 index fd42a12dd..000000000 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/resources/db/testdata/V2_1__PoC_Test_Data.sql +++ /dev/null @@ -1,69 +0,0 @@ --- Test Data to be added after V2 so we can test subsequent migrations - -insert into connector (endpoint, connector_id, created_at, last_refresh_attempt_at, last_successful_refresh_at, - online_status) -values ('https://my-connector.com/api/v1/ids/data', 'test-connector-1', '2019-01-01 00:00:00', - '2019-01-01 00:00:00', '2019-01-01 00:00:00', 'ONLINE'); -insert into data_offer (connector_endpoint, asset_id, asset_properties, created_at, updated_at) -values ('https://my-connector.com/api/v1/ids/data', - 'test-asset-1', - '{ - "asset:prop:id": "test-asset-1" - }', - '2019-01-01 00:00:00', - '2019-01-01 00:00:00'), - ('https://my-connector.com/api/v1/ids/data', - 'test-asset-2', - '{ - "asset:prop:id": "urn:artifact:db-rail-network-2023-jan", - "asset:prop:name": "Rail Network DB 2023 January", - "asset:prop:version": "1.1", - "asset:prop:originator": "https://example-connector.rail-mgmt.bahn.de/api/v1/api/v1/ids/data", - "asset:prop:originatorOrganization": "Deutsche Bahn AG", - "asset:prop:keywords": "db, bahn, rail, Rail-Designer", - "asset:prop:contenttype": "application/json", - "asset:prop:description": "Train Network Map released on 10.01.2023, valid until 31.02.2023. \nFile format is xyz as exported by Rail-Designer.", - "asset:prop:language": "https://w3id.org/idsa/code/EN", - "asset:prop:publisher": "https://my.cool-api.gg/about", - "asset:prop:standardLicense": "https://my.cool-api.gg/license", - "asset:prop:endpointDocumentation": "https://my.cool-api.gg/docs", - "http://w3id.org/mds#dataCategory": "Infrastructure and Logistics", - "http://w3id.org/mds#dataSubcategory": "General Information About Planning Of Routes", - "http://w3id.org/mds#dataModel": "my-data-model-001", - "http://w3id.org/mds#geoReferenceMethod": "my-geo-reference-method", - "http://w3id.org/mds#transportMode": "Rail" - }', - '2019-01-01 00:00:00', - '2019-01-01 00:00:00'); - -insert into data_offer_contract_offer (contract_offer_id, connector_endpoint, asset_id, policy, created_at, updated_at) -values ('test-contract-offer-1', - 'https://my-connector.com/api/v1/ids/data', - 'test-asset-1', - '"test-policy-1"', - '2019-01-01 00:00:00', - '2019-01-01 00:00:00'), - ('test-contract-offer-2', - 'https://my-connector.com/api/v1/ids/data', - 'test-asset-2', - '"test-policy-2"', - '2019-01-01 00:00:00', - '2019-01-01 00:00:00'); - -insert into broker_event_log (created_at, user_message, event, event_status, connector_endpoint, asset_id, error_stack, - duration_in_ms) -values ('2019-01-01 00:00:00', - 'Connector was successfully updated, and changes were incorporated', - 'CONNECTOR_UPDATED', - 'OK', - 'https://my-connector.com/api/v1/ids/data', - 'test-asset-1', - null, - 100); - -insert into broker_execution_time_measurement (connector_endpoint, created_at, type, error_status, duration_in_ms) -values ('https://my-connector.com/api/v1/ids/data', - '2019-01-01 00:00:00', - 'CONNECTOR_REFRESH', - 'OK', - 100); diff --git a/extensions/broker-server/README.md b/extensions/broker-server/README.md deleted file mode 100644 index 103af330b..000000000 --- a/extensions/broker-server/README.md +++ /dev/null @@ -1,38 +0,0 @@ - -
      -
      - - Logo - - -

      EDC-Connector Extension:
      Broker Server

      - -

      - Report Bug - · - Request Feature -

      -
      - -## About this Extension - -Implementation of an EDC Broker backend as an EDC Extension. - -This extension does multiple things: - -- Storage of Connectors and Data Offers -- Connector Crawling -- Connector Discovery -- API implementations for our the full management capabilities of our Broker UI - -## Why does this extension exist? - -To let the broker easily be a part of a data space we are implementing it on an EDC basis. - -## License - -Apache License 2.0 - see [LICENSE](../../LICENSE) - -## Contact - -sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java deleted file mode 100644 index 3b288ea13..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtension.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; -import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; -import org.eclipse.edc.connector.spi.catalog.CatalogService; -import org.eclipse.edc.jsonld.spi.JsonLd; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.spi.types.TypeManager; -import org.eclipse.edc.web.spi.WebService; - -import static de.sovity.edc.ext.brokerserver.services.config.EdcConfigPropertyUtils.toEdcProp; - -public class BrokerServerExtension implements ServiceExtension { - - public static final String EXTENSION_NAME = "BrokerServerExtension"; - - @Setting - public static final String ADMIN_API_KEY = toEdcProp("EDC_BROKER_SERVER_ADMIN_API_KEY"); - - @Setting - public static final String KNOWN_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_CONNECTORS"); - - @Setting - public static final String CRON_ONLINE_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH"); - - @Setting - public static final String CRON_OFFLINE_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH"); - - @Setting - public static final String CRON_DEAD_CONNECTOR_REFRESH = toEdcProp("EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH"); - - @Setting - public static final String NUM_THREADS = toEdcProp("EDC_BROKER_SERVER_NUM_THREADS"); - - @Setting - public static final String HIDE_OFFLINE_DATA_OFFERS_AFTER = toEdcProp("EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER"); - - @Setting - public static final String MAX_DATA_OFFERS_PER_CONNECTOR = toEdcProp("EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR"); - - @Setting - public static final String MAX_CONTRACT_OFFERS_PER_DATA_OFFER = toEdcProp("EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER"); - - @Setting - public static final String CATALOG_PAGE_PAGE_SIZE = toEdcProp("EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE"); - - @Setting - public static final String DEFAULT_CONNECTOR_DATASPACE = toEdcProp("EDC_BROKER_SERVER_DEFAULT_DATASPACE"); - - @Setting - public static final String KNOWN_DATASPACE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS"); - - @Setting - public static final String KILL_OFFLINE_CONNECTORS_AFTER = toEdcProp("EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER"); - - @Setting - public static final String SCHEDULED_KILL_OFFLINE_CONNECTORS = toEdcProp("EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS"); - - @Inject - private ManagementApiConfiguration managementApiConfiguration; - - @Inject - private WebService webService; - - @Inject - private TypeManager typeManager; - - @Inject - private ManagementApiTypeTransformerRegistry typeTransformerRegistry; - - @Inject - private JsonLd jsonLd; - - @Inject - private CatalogService catalogService; - - /** - * Manual Dependency Injection Result - */ - private BrokerServerExtensionContext services; - - @Override - public String name() { - return EXTENSION_NAME; - } - - @Override - public void initialize(ServiceExtensionContext context) { - services = BrokerServerExtensionContextBuilder.buildContext( - context.getConfig(), - context.getMonitor(), - typeManager, - typeTransformerRegistry, - jsonLd, - catalogService - ); - - // This is a hack for tests, so we can access the running context from tests. - BrokerServerExtensionContext.instance = services; - - var managementApiGroup = managementApiConfiguration.getContextAlias(); - webService.registerResource(managementApiGroup, services.brokerServerResource()); - } - - @Override - public void start() { - services.brokerServerInitializer().onStartup(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java deleted file mode 100644 index 3dd8bd2fa..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContext.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; -import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; -import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.FetchedCatalogBuilder; -import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; - - -/** - * Manual Dependency Injection result - * - * @param brokerServerResource REST Resource with API Endpoint implementations - * @param brokerServerInitializer Startup Logic - */ -public record BrokerServerExtensionContext( - BrokerServerResource brokerServerResource, - BrokerServerInitializer brokerServerInitializer, - - // Required for Integration Tests - ConnectorUpdater connectorUpdater, - ConnectorCreator connectorCreator, - PolicyMapper policyMapper, - FetchedCatalogBuilder fetchedCatalogBuilder, - DataOfferRecordUpdater dataOfferRecordUpdater -) { - /** - * This is a hack for our tests. - *

      - * Right now we have no good way to access the context from tests. - */ - public static BrokerServerExtensionContext instance; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java deleted file mode 100644 index 8f78ffa67..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerExtensionContextBuilder.java +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.dao.ContractOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryAvailableFilterFetcher; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryContractOfferFetcher; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryDataOfferFetcher; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFilterService; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQuerySortingService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorDetailQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorListQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.ViewCountLogger; -import de.sovity.edc.ext.brokerserver.db.DataSourceFactory; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.BrokerServerInitializer; -import de.sovity.edc.ext.brokerserver.services.ConnectorCleaner; -import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; -import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; -import de.sovity.edc.ext.brokerserver.services.KnownConnectorsInitializer; -import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorQueryService; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; -import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorListApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorOnlineStatusMapper; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorService; -import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; -import de.sovity.edc.ext.brokerserver.services.api.DataOfferMappingUtils; -import de.sovity.edc.ext.brokerserver.services.api.PaginationMetadataUtils; -import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterAttributeDefinitionService; -import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; -import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogSearchService; -import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettingsFactory; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; -import de.sovity.edc.ext.brokerserver.services.queue.ThreadPool; -import de.sovity.edc.ext.brokerserver.services.queue.ThreadPoolTaskQueue; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateFailureWriter; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdateSuccessWriter; -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.CatalogFetcher; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.ContractOfferRecordUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchApplier; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferPatchBuilder; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferRecordUpdater; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.FetchedCatalogBuilder; -import de.sovity.edc.ext.brokerserver.services.schedules.DeadConnectorRefreshJob; -import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorKillerJob; -import de.sovity.edc.ext.brokerserver.services.schedules.OfflineConnectorRefreshJob; -import de.sovity.edc.ext.brokerserver.services.schedules.OnlineConnectorRefreshJob; -import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; -import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; -import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; -import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; -import de.sovity.edc.utils.catalog.DspCatalogService; -import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; -import lombok.NoArgsConstructor; -import org.eclipse.edc.connector.spi.catalog.CatalogService; -import org.eclipse.edc.jsonld.spi.JsonLd; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.spi.CoreConstants; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.system.configuration.Config; -import org.eclipse.edc.spi.types.TypeManager; -import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - - -/** - * Manual Dependency Injection (DYDI). - *

      - * We want to develop as Java Backend Development is done, but we have - * no CDI / DI Framework to rely on. - *

      - * EDC {@link Inject} only works in {@link BrokerServerExtension}. - */ -@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) -public class BrokerServerExtensionContextBuilder { - - public static BrokerServerExtensionContext buildContext( - Config config, - Monitor monitor, - TypeManager typeManager, - TypeTransformerRegistry typeTransformerRegistry, - JsonLd jsonLd, - CatalogService catalogService - ) { - var brokerServerSettingsFactory = new BrokerServerSettingsFactory(config, monitor); - var brokerServerSettings = brokerServerSettingsFactory.buildBrokerServerSettings(); - var adminApiKeyValidator = new AdminApiKeyValidator(brokerServerSettings); - - // Dao - var dataOfferQueries = new DataOfferQueries(); - var dataSourceFactory = new DataSourceFactory(config); - var dataSource = dataSourceFactory.newDataSource(); - var dslContextFactory = new DslContextFactory(dataSource); - var connectorQueries = new ConnectorQueries(); - var catalogQuerySortingService = new CatalogQuerySortingService(); - var catalogSearchService = new CatalogSearchService(); - var catalogQueryFilterService = new CatalogQueryFilterService(brokerServerSettings, catalogSearchService); - var catalogQueryContractOfferFetcher = new CatalogQueryContractOfferFetcher(); - var catalogQueryDataOfferFetcher = new CatalogQueryDataOfferFetcher( - catalogQuerySortingService, - catalogQueryFilterService, - catalogQueryContractOfferFetcher - ); - var catalogQueryAvailableFilterFetcher = new CatalogQueryAvailableFilterFetcher(catalogQueryFilterService); - var catalogQueryService = new CatalogQueryService( - catalogQueryDataOfferFetcher, - catalogQueryAvailableFilterFetcher, - brokerServerSettings - ); - var connectorListQueryService = new ConnectorListQueryService(); - var connectorDetailQueryService = new ConnectorDetailQueryService(); - var dataOfferDetailPageQueryService = new DataOfferDetailPageQueryService( - catalogQueryContractOfferFetcher, brokerServerSettings); - - - // Services - var objectMapperJsonLd = getJsonLdObjectMapper(typeManager); - var brokerEventLogger = new BrokerEventLogger(); - var brokerExecutionTimeLogger = new BrokerExecutionTimeLogger(); - var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); - var dataOfferRecordUpdater = new DataOfferRecordUpdater(); - var contractOfferQueries = new ContractOfferQueries(); - var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(brokerServerSettings, brokerEventLogger); - var dataOfferPatchBuilder = new DataOfferPatchBuilder( - contractOfferQueries, - dataOfferQueries, - dataOfferRecordUpdater, - contractOfferRecordUpdater - ); - var dataOfferPatchApplier = new DataOfferPatchApplier(); - var dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); - var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter( - brokerEventLogger, - dataOfferWriter, - dataOfferLimitsEnforcer - ); - var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd); - var fetchedDataOfferBuilder = new FetchedCatalogBuilder(assetMapper); - var dspDataOfferBuilder = new DspDataOfferBuilder(jsonLd); - var dspCatalogService = new DspCatalogService( - catalogService, - dspDataOfferBuilder - ); - var dataOfferFetcher = new CatalogFetcher(dspCatalogService, fetchedDataOfferBuilder); - var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(brokerEventLogger, monitor); - var connectorUpdater = new ConnectorUpdater( - dataOfferFetcher, - connectorUpdateSuccessWriter, - connectorUpdateFailureWriter, - connectorQueries, - dslContextFactory, - monitor, - brokerExecutionTimeLogger - ); - var paginationMetadataUtils = new PaginationMetadataUtils(); - var threadPoolTaskQueue = new ThreadPoolTaskQueue(); - var threadPool = new ThreadPool(threadPoolTaskQueue, brokerServerSettings, monitor); - var connectorQueue = new ConnectorQueue(connectorUpdater, threadPool); - var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); - var connectorCreator = new ConnectorCreator(connectorQueries); - var knownConnectorsInitializer = new KnownConnectorsInitializer( - config, - connectorQueue, - connectorCreator - ); - var catalogFilterAttributeDefinitionService = new CatalogFilterAttributeDefinitionService(); - var catalogFilterService = new CatalogFilterService(catalogFilterAttributeDefinitionService); - var viewCountLogger = new ViewCountLogger(); - var connectorService = new ConnectorService(connectorCreator, connectorQueue); - var connectorKiller = new ConnectorKiller(); - var connectorClearer = new ConnectorCleaner(); - var offlineConnectorKiller = new OfflineConnectorKiller( - brokerServerSettings, - connectorQueries, - brokerEventLogger, - connectorKiller, - connectorClearer - ); - var operatorMapper = new OperatorMapper(); - var literalMapper = new LiteralMapper( - objectMapperJsonLd - ); - var atomicConstraintMapper = new AtomicConstraintMapper( - literalMapper, - operatorMapper - ); - var policyValidator = new PolicyValidator(); - var constraintExtractor = new ConstraintExtractor( - policyValidator, - atomicConstraintMapper - ); - var policyMapper = new PolicyMapper( - constraintExtractor, - atomicConstraintMapper, - typeTransformerRegistry - ); - var dataOfferMappingUtils = new DataOfferMappingUtils( - policyMapper, - assetMapper - ); - var connectorOnlineStatusMapper = new ConnectorOnlineStatusMapper(); - - // Schedules - List> jobs = List.of( - getOnlineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), - getOfflineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), - getDeadConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), - getOfflineConnectorKillerCronJob(dslContextFactory, offlineConnectorKiller) - ); - - // Startup - var quartzScheduleInitializer = new QuartzScheduleInitializer(config, monitor, jobs); - var brokerServerInitializer = new BrokerServerInitializer( - dslContextFactory, - knownConnectorsInitializer, - quartzScheduleInitializer - ); - - // UI Capabilities - var catalogApiService = new CatalogApiService( - paginationMetadataUtils, - catalogQueryService, - dataOfferMappingUtils, - catalogFilterService, - brokerServerSettings - ); - var connectorApiService = new ConnectorApiService( - connectorService, - brokerEventLogger, - connectorQueries - ); - var dataOfferDetailApiService = new DataOfferDetailApiService( - dataOfferDetailPageQueryService, - viewCountLogger, - dataOfferMappingUtils - ); - var connectorQueryService = new AuthorityPortalConnectorQueryService(); - var dataOfferCountApiService = new AuthorityPortalConnectorMetadataApiService( - connectorQueryService, - connectorOnlineStatusMapper - ); - var connectorDetailApiService = new ConnectorDetailApiService(connectorDetailQueryService, connectorOnlineStatusMapper); - var connectorListApiService = new ConnectorListApiService(connectorListQueryService, connectorOnlineStatusMapper, paginationMetadataUtils); - var authorityPortalOrganizationMetadataApiService = new AuthorityPortalOrganizationMetadataApiService(); - var authorityPortalDataOfferApiService = new AuthorityPortalConnectorDataOfferApiService(connectorQueryService, connectorOnlineStatusMapper); - var brokerServerResource = new BrokerServerResourceImpl( - dslContextFactory, - connectorApiService, - connectorListApiService, - connectorDetailApiService, - catalogApiService, - dataOfferDetailApiService, - adminApiKeyValidator, - dataOfferCountApiService, - authorityPortalDataOfferApiService, - authorityPortalOrganizationMetadataApiService - ); - - return new BrokerServerExtensionContext( - brokerServerResource, - brokerServerInitializer, - connectorUpdater, - connectorCreator, - policyMapper, - fetchedDataOfferBuilder, - dataOfferRecordUpdater - ); - } - - @NotNull - private static CronJobRef getOfflineConnectorKillerCronJob(DslContextFactory dslContextFactory, OfflineConnectorKiller offlineConnectorKiller) { - return new CronJobRef<>( - BrokerServerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, - OfflineConnectorKillerJob.class, - () -> new OfflineConnectorKillerJob(dslContextFactory, offlineConnectorKiller) - ); - } - - @NotNull - private static CronJobRef getOnlineConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { - return new CronJobRef<>( - BrokerServerExtension.CRON_ONLINE_CONNECTOR_REFRESH, - OnlineConnectorRefreshJob.class, - () -> new OnlineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) - ); - } - - @NotNull - private static CronJobRef getOfflineConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { - return new CronJobRef<>( - BrokerServerExtension.CRON_OFFLINE_CONNECTOR_REFRESH, - OfflineConnectorRefreshJob.class, - () -> new OfflineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) - ); - } - - @NotNull - private static CronJobRef getDeadConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { - return new CronJobRef<>( - BrokerServerExtension.CRON_DEAD_CONNECTOR_REFRESH, - DeadConnectorRefreshJob.class, - () -> new DeadConnectorRefreshJob(dslContextFactory, connectorQueueFiller) - ); - } - - private static ObjectMapper getJsonLdObjectMapper(TypeManager typeManager) { - var objectMapper = typeManager.getMapper(CoreConstants.JSON_LD); - - // Fixes Dates in JSON-LD Object Mapper - // The Core EDC uses longs over OffsetDateTime, so they never fixed the date format - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - - return objectMapper; - } - - @NotNull - private static AssetMapper newAssetMapper(TypeTransformerRegistry typeTransformerRegistry, JsonLd jsonLd) { - var edcPropertyUtils = new EdcPropertyUtils(); - var assetJsonLdUtils = new AssetJsonLdUtils(); - var assetEditRequestMapper = new AssetEditRequestMapper(); - var shortDescriptionBuilder = new ShortDescriptionBuilder(); - var assetJsonLdParser = new AssetJsonLdParser( - assetJsonLdUtils, - shortDescriptionBuilder, - endpoint -> false - ); - var httpHeaderMapper = new HttpHeaderMapper(); - var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); - var dataSourceMapper = new DataSourceMapper( - edcPropertyUtils, - httpDataSourceMapper - ); - var assetJsonLdBuilder = new AssetJsonLdBuilder( - dataSourceMapper, - assetJsonLdParser, - assetEditRequestMapper - ); - return new AssetMapper( - typeTransformerRegistry, - assetJsonLdBuilder, - assetJsonLdParser, - jsonLd - ); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java deleted file mode 100644 index 7f38ea0d1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/BrokerServerResourceImpl.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import de.sovity.edc.ext.brokerserver.api.BrokerServerResource; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadataRequest; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorCreationRequest; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorDataOfferApiService; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalConnectorMetadataApiService; -import de.sovity.edc.ext.brokerserver.services.api.AuthorityPortalOrganizationMetadataApiService; -import de.sovity.edc.ext.brokerserver.services.api.CatalogApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorDetailApiService; -import de.sovity.edc.ext.brokerserver.services.api.ConnectorListApiService; -import de.sovity.edc.ext.brokerserver.services.api.DataOfferDetailApiService; -import de.sovity.edc.ext.brokerserver.services.config.AdminApiKeyValidator; -import lombok.RequiredArgsConstructor; - -import java.util.List; - - -/** - * Implementation of {@link BrokerServerResource} - */ -@RequiredArgsConstructor -public class BrokerServerResourceImpl implements BrokerServerResource { - private final DslContextFactory dslContextFactory; - private final ConnectorApiService connectorApiService; - private final ConnectorListApiService connectorListApiService; - private final ConnectorDetailApiService connectorDetailApiService; - private final CatalogApiService catalogApiService; - private final DataOfferDetailApiService dataOfferDetailApiService; - private final AdminApiKeyValidator adminApiKeyValidator; - private final AuthorityPortalConnectorMetadataApiService authorityPortalConnectorMetadataApiService; - private final AuthorityPortalConnectorDataOfferApiService authorityPortalConnectorDataOffersApiService; - private final AuthorityPortalOrganizationMetadataApiService authorityPortalOrganizationMetadataApiService; - - @Override - public CatalogPageResult catalogPage(CatalogPageQuery query) { - return dslContextFactory.transactionResult(dsl -> catalogApiService.catalogPage(dsl, query)); - } - - @Override - public ConnectorPageResult connectorPage(ConnectorPageQuery query) { - return dslContextFactory.transactionResult(dsl -> connectorListApiService.connectorListPage(dsl, query)); - } - - @Override - public DataOfferDetailPageResult dataOfferDetailPage(DataOfferDetailPageQuery query) { - return dslContextFactory.transactionResult(dsl -> dataOfferDetailApiService.dataOfferDetailPage(dsl, query)); - } - - @Override - public ConnectorDetailPageResult connectorDetailPage(ConnectorDetailPageQuery query) { - return dslContextFactory.transactionResult(dsl -> connectorDetailApiService.connectorDetailPage(dsl, query)); - } - - @Override - public void addConnectors(List endpoints, String adminApiKey) { - adminApiKeyValidator.validateAdminApiKey(adminApiKey); - dslContextFactory.transaction(dsl -> connectorApiService.addConnectors(dsl, endpoints)); - } - - @Override - public void addConnectorsWithMdsIds(ConnectorCreationRequest connectors, String adminApiKey) { - adminApiKeyValidator.validateAdminApiKey(adminApiKey); - dslContextFactory.transaction(dsl -> connectorApiService.addConnectorsWithMdsIds(dsl, connectors)); - } - - @Override - public void deleteConnectors(List endpoints, String adminApiKey) { - adminApiKeyValidator.validateAdminApiKey(adminApiKey); - dslContextFactory.transaction(dsl -> connectorApiService.deleteConnectors(dsl, endpoints)); - } - - @Override - public List getConnectorMetadata(List endpoints, String adminApiKey) { - adminApiKeyValidator.validateAdminApiKey(adminApiKey); - return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorMetadataApiService.getMetadataByEndpoints(dsl, endpoints)); - } - - @Override - public void setOrganizationMetadata(AuthorityPortalOrganizationMetadataRequest organizationMetadataRequest, String adminApiKey) { - adminApiKeyValidator.validateAdminApiKey(adminApiKey); - dslContextFactory.transaction(dsl -> authorityPortalOrganizationMetadataApiService.setOrganizationMetadata(dsl, organizationMetadataRequest.getOrganizations())); - } - - @Override - public List getConnectorDataOffers(List endpoints, String adminApiKey) { - adminApiKeyValidator.validateAdminApiKey(adminApiKey); - return dslContextFactory.transactionResult(dsl -> authorityPortalConnectorDataOffersApiService.getConnectorDataOffersByEndpoints(dsl, endpoints)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java deleted file mode 100644 index 31fd44a7f..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ConnectorQueries.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao; - -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import org.jooq.DSLContext; - -import java.time.Duration; -import java.time.OffsetDateTime; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -public class ConnectorQueries { - - public ConnectorRecord findByEndpoint(DSLContext dsl, String endpoint) { - var c = Tables.CONNECTOR; - return dsl.selectFrom(c).where(c.ENDPOINT.eq(endpoint)).fetchOne(); - } - - public Set findConnectorsForScheduledRefresh(DSLContext dsl, ConnectorOnlineStatus onlineStatus) { - var c = Tables.CONNECTOR; - return dsl.select(c.ENDPOINT).from(c).where(c.ONLINE_STATUS.eq(onlineStatus)).fetchSet(c.ENDPOINT); - } - - public Set findExistingConnectors(DSLContext dsl, Collection connectorEndpoints) { - var c = Tables.CONNECTOR; - return dsl.select(c.ENDPOINT).from(c) - .where(PostgresqlUtils.in(c.ENDPOINT, connectorEndpoints)) - .fetchSet(c.ENDPOINT); - } - - public List findAllConnectorsForKilling(DSLContext dsl, Duration deleteOfflineConnectorsAfter) { - var c = Tables.CONNECTOR; - return dsl.select(c.ENDPOINT).from(c) - .where(c.LAST_SUCCESSFUL_REFRESH_AT.lt(OffsetDateTime.now().minus(deleteOfflineConnectorsAfter))) - .fetch(c.ENDPOINT); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java deleted file mode 100644 index ee3fdd7aa..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryAvailableFilterFetcher.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; -import lombok.RequiredArgsConstructor; -import org.jooq.Field; -import org.jooq.JSON; -import org.jooq.impl.DSL; -import org.jooq.impl.SQLDataType; - -import java.util.ArrayList; -import java.util.List; - -@RequiredArgsConstructor -public class CatalogQueryAvailableFilterFetcher { - private final CatalogQueryFilterService catalogQueryFilterService; - - /** - * Query available filter values. - * - * @param fields query fields - * @param searchQuery search query - * @param filters filters (values + filter clauses) - * @return {@link Field} with field[iFilter][iValue] - */ - public Field queryAvailableFilterValues( - CatalogQueryFields fields, - String searchQuery, - List filters - ) { - List> resultFields = new ArrayList<>(); - for (int i = 0; i < filters.size(); i++) { - // When querying a filter's values we apply all filters except for the current filter's values - var currentFilter = filters.get(i); - var otherFilters = CollectionUtils2.allElementsExceptForIndex(filters, i); - var resultField = queryFilterValues(fields, currentFilter, searchQuery, otherFilters); - resultFields.add(resultField); - } - return DSL.select(DSL.jsonArray(resultFields)).asField(); - } - - private Field queryFilterValues( - CatalogQueryFields parentQueryFields, - CatalogQueryFilter currentFilter, - String searchQuery, - List otherFilters - ) { - var fields = parentQueryFields.withSuffix("filter_" + currentFilter.name()); - var c = fields.getConnectorTable(); - var d = fields.getDataOfferTable(); - - var value = currentFilter.valueQuery().getAttributeValueField(fields); - - return DSL.select(DSL.coalesce(DSL.arrayAggDistinct(value), DSL.array().cast(SQLDataType.VARCHAR.array()))) - .from(d) - .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) - .where(catalogQueryFilterService.filterDbQuery(fields, searchQuery, otherFilters)) - .asField(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java deleted file mode 100644 index d2e370c5c..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryContractOfferFetcher.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; -import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; -import lombok.RequiredArgsConstructor; -import org.jooq.Field; -import org.jooq.impl.DSL; - -import java.util.List; - -@RequiredArgsConstructor -public class CatalogQueryContractOfferFetcher { - - /** - * Query a data offer's contract offers. - * - * @param d Data offer table - * @return {@link Field} of {@link ContractOfferRs}s - */ - public Field> getContractOffers(DataOffer d) { - var co = Tables.CONTRACT_OFFER; - - var query = DSL.select( - co.CONTRACT_OFFER_ID, - co.POLICY.cast(String.class).as("policyJson"), - co.CREATED_AT, - co.UPDATED_AT - ).from(co).where( - co.CONNECTOR_ENDPOINT.eq(d.CONNECTOR_ENDPOINT), - co.ASSET_ID.eq(d.ASSET_ID)).orderBy(co.CREATED_AT.desc() - ); - - return MultisetUtils.multiset(query, ContractOfferRs.class); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java deleted file mode 100644 index 24cbb3282..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryDataOfferFetcher.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; -import de.sovity.edc.ext.brokerserver.dao.utils.MultisetUtils; -import lombok.RequiredArgsConstructor; -import org.jooq.Field; -import org.jooq.Record; -import org.jooq.SelectOnConditionStep; -import org.jooq.SelectSelectStep; -import org.jooq.impl.DSL; - -import java.util.List; - -@RequiredArgsConstructor -public class CatalogQueryDataOfferFetcher { - private final CatalogQuerySortingService catalogQuerySortingService; - private final CatalogQueryFilterService catalogQueryFilterService; - private final CatalogQueryContractOfferFetcher catalogQueryContractOfferFetcher; - - /** - * Query data offers - * - * @param fields query fields - * @param searchQuery search query - * @param filters filters (queries + filter clauses) - * @param sorting sorting - * @param pageQuery pagination - * @return {@link Field} of {@link DataOfferListEntryRs}s - */ - public Field> queryDataOffers( - CatalogQueryFields fields, - String searchQuery, - List filters, - CatalogPageSortingType sorting, - PageQuery pageQuery - ) { - var c = fields.getConnectorTable(); - var d = fields.getDataOfferTable(); - - var select = DSL.select( - d.ASSET_ID.as("assetId"), - d.ASSET_JSON_LD.cast(String.class).as("assetJsonLd"), - d.CREATED_AT, - d.UPDATED_AT, - catalogQueryContractOfferFetcher.getContractOffers(d).as("contractOffers"), - c.ENDPOINT.as("connectorEndpoint"), - c.ONLINE_STATUS.as("connectorOnlineStatus"), - c.PARTICIPANT_ID.as("connectorParticipantId"), - fields.getOrganizationName().as("organizationName"), - fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt") - ); - - var query = from(select, fields) - .where(catalogQueryFilterService.filterDbQuery(fields, searchQuery, filters)) - .orderBy(catalogQuerySortingService.getOrderBy(fields, sorting)) - .limit(pageQuery.offset(), pageQuery.limit()); - - return MultisetUtils.multiset(query, DataOfferListEntryRs.class); - } - - /** - * Query number of data offers - * - * @param fields query fields - * @param searchQuery search query - * @param filters filters (queries + filter clauses) - * @return {@link Field} with number of data offers - */ - public Field queryNumDataOffers(CatalogQueryFields fields, String searchQuery, List filters) { - var query = from(DSL.select(DSL.count()), fields) - .where(catalogQueryFilterService.filterDbQuery(fields, searchQuery, filters)); - return DSL.field(query); - } - - private SelectOnConditionStep from(SelectSelectStep select, CatalogQueryFields fields) { - var c = fields.getConnectorTable(); - var d = fields.getDataOfferTable(); - return select.from(d).leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java deleted file mode 100644 index 747f0707d..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFields.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOffer; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.DataOfferViewCount; -import de.sovity.edc.ext.brokerserver.services.config.DataSpaceConfig; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.experimental.FieldDefaults; -import org.jooq.Field; -import org.jooq.Table; -import org.jooq.impl.DSL; - -import java.time.OffsetDateTime; - -import static org.jooq.impl.DSL.coalesce; - -/** - * Tables and fields used in the catalog page query. - *

      - * Having this as a class makes access to computed fields (e.g. asset properties) easier. - */ -@Getter -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -public class CatalogQueryFields { - Connector connectorTable; - DataOffer dataOfferTable; - DataOfferViewCount dataOfferViewCountTable; - - // Asset Properties from JSON to be used in sorting / filtering - Field dataSpace; - - // This date should always be non-null - // It's used in the UI to display the last relevant change date of a connector - Field offlineSinceOrLastUpdatedAt; - - DataSpaceConfig dataSpaceConfig; - - public CatalogQueryFields( - Connector connectorTable, - DataOffer dataOfferTable, - DataOfferViewCount dataOfferViewCountTable, - DataSpaceConfig dataSpaceConfig - ) { - this.connectorTable = connectorTable; - this.dataOfferTable = dataOfferTable; - this.dataOfferViewCountTable = dataOfferViewCountTable; - this.dataSpaceConfig = dataSpaceConfig; - offlineSinceOrLastUpdatedAt = offlineSinceOrLastUpdatedAt(connectorTable); - - dataSpace = buildDataSpaceField(connectorTable, dataSpaceConfig); - } - - private Field buildDataSpaceField(Connector connectorTable, DataSpaceConfig dataSpaceConfig) { - var endpoint = connectorTable.ENDPOINT; - - var connectors = dataSpaceConfig.dataSpaceConnectors(); - if (connectors.isEmpty()) { - return DSL.val(dataSpaceConfig.defaultDataSpace()); - } - - var first = connectors.get(0); - var dspCase = DSL.case_(endpoint).when(first.endpoint(), first.dataSpaceName()); - - for (var dsp : connectors.subList(1, connectors.size())) { - dspCase = dspCase.when(dsp.endpoint(), dsp.dataSpaceName()); - } - - return dspCase.else_(DSL.val(dataSpaceConfig.defaultDataSpace())); - } - - public CatalogQueryFields withSuffix(String additionalSuffix) { - return new CatalogQueryFields( - connectorTable.as(withSuffix(connectorTable, additionalSuffix)), - dataOfferTable.as(withSuffix(dataOfferTable, additionalSuffix)), - dataOfferViewCountTable.as(withSuffix(dataOfferViewCountTable, additionalSuffix)), - dataSpaceConfig - ); - } - - private String withSuffix(Table table, String additionalSuffix) { - return "%s_%s".formatted(table.getName(), additionalSuffix); - } - - public Field getViewCount() { - var subquery = DSL.select(DSL.count()) - .from(dataOfferViewCountTable) - .where(dataOfferViewCountTable.ASSET_ID.eq(dataOfferTable.ASSET_ID) - .and(dataOfferViewCountTable.CONNECTOR_ENDPOINT.eq(connectorTable.ENDPOINT))); - - return subquery.asField(); - } - - public Field getOrganizationName() { - return organizationName(connectorTable.MDS_ID); - } - - public static Field offlineSinceOrLastUpdatedAt(Connector connectorTable) { - return DSL.coalesce( - connectorTable.LAST_SUCCESSFUL_REFRESH_AT, - connectorTable.CREATED_AT - ); - } - - public static Field organizationName(Field mdsId) { - var om = Tables.ORGANIZATION_METADATA; - var organizationName = DSL.select(om.NAME) - .from(om) - .where(om.MDS_ID.eq(mdsId)) - .asField() - .cast(String.class); - return coalesce(organizationName, "Unknown"); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java deleted file mode 100644 index b92af8920..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryFilterService.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogSearchService; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jooq.Condition; -import org.jooq.impl.DSL; - -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -@RequiredArgsConstructor -public class CatalogQueryFilterService { - private final BrokerServerSettings brokerServerSettings; - private final CatalogSearchService catalogSearchService; - - public Condition filterDbQuery(CatalogQueryFields fields, String searchQuery, List filters) { - var conditions = new ArrayList(); - conditions.add(catalogSearchService.filterBySearch(fields, searchQuery)); - conditions.add(onlyOnlineOrRecentlyOfflineConnectors(fields.getConnectorTable())); - conditions.addAll(filters.stream().map(CatalogQueryFilter::queryFilterClauseOrNull) - .filter(Objects::nonNull).map(it -> it.filterDataOffers(fields)).toList()); - return DSL.and(conditions); - } - - @NotNull - private Condition onlyOnlineOrRecentlyOfflineConnectors(Connector c) { - var maxOfflineDuration = brokerServerSettings.getHideOfflineDataOffersAfter(); - - Condition maxOfflineDurationNotExceeded; - if (maxOfflineDuration == null) { - maxOfflineDurationNotExceeded = DSL.trueCondition(); - } else { - maxOfflineDurationNotExceeded = c.LAST_SUCCESSFUL_REFRESH_AT.greaterThan(OffsetDateTime.now().minus(maxOfflineDuration)); - } - - return DSL.or( - c.ONLINE_STATUS.eq(ConnectorOnlineStatus.ONLINE), - maxOfflineDurationNotExceeded - ); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java deleted file mode 100644 index 04a585e35..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQueryService.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogPageRs; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.List; - -@RequiredArgsConstructor -public class CatalogQueryService { - private final CatalogQueryDataOfferFetcher catalogQueryDataOfferFetcher; - private final CatalogQueryAvailableFilterFetcher catalogQueryAvailableFilterFetcher; - private final BrokerServerSettings brokerServerSettings; - - /** - * Query all data required for the catalog page - * - * @param dsl transaction - * @param searchQuery search query - * @param filters filters (queries + filter clauses) - * @param sorting sorting - * @param pageQuery pagination - * @return {@link CatalogPageRs} - */ - public CatalogPageRs queryCatalogPage( - DSLContext dsl, - String searchQuery, - List filters, - CatalogPageSortingType sorting, - PageQuery pageQuery - ) { - var fields = new CatalogQueryFields( - Tables.CONNECTOR, - Tables.DATA_OFFER, - Tables.DATA_OFFER_VIEW_COUNT, - brokerServerSettings.getDataSpaceConfig() - ); - - var availableFilterValues = catalogQueryAvailableFilterFetcher - .queryAvailableFilterValues(fields, searchQuery, filters); - - var dataOffers = catalogQueryDataOfferFetcher.queryDataOffers(fields, searchQuery, filters, sorting, pageQuery); - - var numTotalDataOffers = catalogQueryDataOfferFetcher.queryNumDataOffers(fields, searchQuery, filters); - - return dsl.select( - dataOffers.as("dataOffers"), - availableFilterValues.as("availableFilterValues"), - numTotalDataOffers.as("numTotalDataOffers") - ).fetchOneInto(CatalogPageRs.class); - } - -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java deleted file mode 100644 index c06aaecb1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/CatalogQuerySortingService.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog; - -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jooq.OrderField; - -import java.util.List; - -@RequiredArgsConstructor -public class CatalogQuerySortingService { - @NotNull - public List> getOrderBy(CatalogQueryFields fields, @NonNull CatalogPageSortingType sorting) { - List> orderBy; - if (sorting == CatalogPageSortingType.TITLE) { - orderBy = List.of( - fields.getDataOfferTable().ASSET_TITLE.asc(), - fields.getConnectorTable().ENDPOINT.asc() - ); - } else if (sorting == CatalogPageSortingType.MOST_RECENT) { - orderBy = List.of( - fields.getDataOfferTable().CREATED_AT.desc(), - fields.getConnectorTable().ENDPOINT.asc() - ); - } else if (sorting == CatalogPageSortingType.ORIGINATOR) { - orderBy = List.of( - fields.getConnectorTable().ENDPOINT.asc(), - fields.getDataOfferTable().ASSET_TITLE.asc() - ); - } else if (sorting == CatalogPageSortingType.VIEW_COUNT) { - orderBy = List.of( - fields.getViewCount().desc(), - fields.getConnectorTable().ENDPOINT.asc() - ); - } else { - throw new IllegalArgumentException("Unknown %s: %s".formatted(CatalogPageSortingType.class.getName(), sorting)); - } - return orderBy; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java deleted file mode 100644 index 9dabec513..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/AvailableFilterValuesQuery.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import org.jooq.Field; - -@FunctionalInterface -public interface AvailableFilterValuesQuery { - - /** - * Gets the values for a given filter attribute from a list of data offers. - * - * @param fields a - * @return field / multiset field that will contain the available values - */ - Field getAttributeValueField(CatalogQueryFields fields); -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java deleted file mode 100644 index 88a199df1..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogPageRs.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.util.List; - -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class CatalogPageRs { - String availableFilterValues; - List dataOffers; - int numTotalDataOffers; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java deleted file mode 100644 index 9dcccd038..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQueryFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; - -import lombok.NonNull; - -public record CatalogQueryFilter( - @NonNull String name, - @NonNull AvailableFilterValuesQuery valueQuery, - CatalogQuerySelectedFilterQuery queryFilterClauseOrNull -) { -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java deleted file mode 100644 index a3b2bcfa3..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/CatalogQuerySelectedFilterQuery.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import org.jooq.Condition; - -@FunctionalInterface -public interface CatalogQuerySelectedFilterQuery { - - /** - * Adds a filter to a Catalog Query. - * - * @param fields fields and tables available in the catalog query - * @return {@link Condition} - */ - Condition filterDataOffers(CatalogQueryFields fields); -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java deleted file mode 100644 index 1c97cf430..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/DataOfferListEntryRs.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; - -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; -import java.util.List; - -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class DataOfferListEntryRs { - String assetId; - String assetJsonLd; - OffsetDateTime createdAt; - OffsetDateTime updatedAt; - List contractOffers; - String connectorEndpoint; - ConnectorOnlineStatus connectorOnlineStatus; - String connectorParticipantId; - String organizationName; - OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java deleted file mode 100644 index c5cd659bb..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/catalog/models/PageQuery.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.catalog.models; - -public record PageQuery(int offset, int limit) { -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java deleted file mode 100644 index f4acc2589..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorDetailQueryService.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.connector; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorDetailsRs; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; -import org.jooq.Field; -import org.jooq.impl.DSL; - -import java.math.BigDecimal; - -public class ConnectorDetailQueryService { - public ConnectorDetailsRs queryConnectorDetailPage(DSLContext dsl, String connectorEndpoint) { - var c = Tables.CONNECTOR; - - return dsl.select( - c.ENDPOINT.as("endpoint"), - c.PARTICIPANT_ID.as("participantId"), - CatalogQueryFields.organizationName(c.MDS_ID).as("organizationName"), - c.CREATED_AT.as("createdAt"), - c.LAST_SUCCESSFUL_REFRESH_AT.as("lastSuccessfulRefreshAt"), - c.LAST_REFRESH_ATTEMPT_AT.as("lastRefreshAttemptAt"), - c.ONLINE_STATUS.as("onlineStatus"), - dataOfferCount(c.ENDPOINT).as("numDataOffers"), - getAvgSuccessfulCrawlTimeInMs(c).as("connectorCrawlingTimeAvg")) - .from(c) - .where(c.ENDPOINT.eq(connectorEndpoint)) - .groupBy(c.ENDPOINT) - .fetchOneInto(ConnectorDetailsRs.class); - } - - @NotNull - private Field getAvgSuccessfulCrawlTimeInMs(Connector c) { - var betm = Tables.BROKER_EXECUTION_TIME_MEASUREMENT; - return DSL.select(DSL.avg(betm.DURATION_IN_MS)) - .from(betm) - .where(betm.CONNECTOR_ENDPOINT.eq(c.ENDPOINT), betm.ERROR_STATUS.eq(MeasurementErrorStatus.OK)) - .asField(); - } - - private Field dataOfferCount(Field endpoint) { - var d = Tables.DATA_OFFER; - return DSL.select(DSL.count()).from(d).where(d.CONNECTOR_ENDPOINT.eq(endpoint)).asField(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java deleted file mode 100644 index 524631b69..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/ConnectorListQueryService.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.connector; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; -import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector; -import lombok.NonNull; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; -import org.jooq.Field; -import org.jooq.OrderField; -import org.jooq.impl.DSL; - -import java.util.List; - -public class ConnectorListQueryService { - public List queryConnectorPage(DSLContext dsl, String searchQuery, ConnectorPageSortingType sorting) { - var c = Tables.CONNECTOR; - var filterBySearchQuery = SearchUtils.simpleSearch(searchQuery, List.of( - c.ENDPOINT, - c.PARTICIPANT_ID, - CatalogQueryFields.organizationName(c.MDS_ID) - )); - - return dsl.select( - c.ENDPOINT.as("endpoint"), - c.PARTICIPANT_ID.as("participantId"), - CatalogQueryFields.organizationName(c.MDS_ID).as("organizationName"), - c.CREATED_AT.as("createdAt"), - c.LAST_SUCCESSFUL_REFRESH_AT.as("lastSuccessfulRefreshAt"), - c.LAST_REFRESH_ATTEMPT_AT.as("lastRefreshAttemptAt"), - c.ONLINE_STATUS.as("onlineStatus"), - dataOfferCount(c.ENDPOINT).as("numDataOffers") - ) - .from(c) - .where(filterBySearchQuery) - .orderBy(sortConnectorPage(c, sorting)) - .fetchInto(ConnectorListEntryRs.class); - } - - @NotNull - private List> sortConnectorPage(Connector c, @NonNull ConnectorPageSortingType sorting) { - var alphabetically = c.ENDPOINT.asc(); - var recentFirst = c.CREATED_AT.desc(); - var onlineStatus = DSL.case_(c.ONLINE_STATUS) - .when(ConnectorOnlineStatus.ONLINE, 1) - .when(ConnectorOnlineStatus.OFFLINE, 2) - .else_(3) - .asc(); - - if (sorting == ConnectorPageSortingType.ONLINE_STATUS) { - return List.of(onlineStatus, alphabetically); - } else if (sorting == ConnectorPageSortingType.TITLE) { - return List.of(alphabetically, recentFirst); - } else if (sorting == ConnectorPageSortingType.MOST_RECENT) { - return List.of(recentFirst, alphabetically); - } - - throw new IllegalArgumentException("Unhandled sorting type: " + sorting); - } - - private Field dataOfferCount(Field endpoint) { - var d = Tables.DATA_OFFER; - return DSL.select(DSL.count()).from(d).where(d.CONNECTOR_ENDPOINT.eq(endpoint)).asField(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java deleted file mode 100644 index 071d8e5ce..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorDetailsRs.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.connector.model; - -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ConnectorDetailsRs { - String endpoint; - String participantId; - String organizationName; - OffsetDateTime createdAt; - OffsetDateTime lastSuccessfulRefreshAt; - OffsetDateTime lastRefreshAttemptAt; - ConnectorOnlineStatus onlineStatus; - Integer numDataOffers; - Long connectorCrawlingTimeAvg; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java deleted file mode 100644 index 96ba710f6..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/connector/model/ConnectorListEntryRs.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.connector.model; - -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ConnectorListEntryRs { - String endpoint; - String participantId; - String organizationName; - OffsetDateTime createdAt; - OffsetDateTime lastSuccessfulRefreshAt; - OffsetDateTime lastRefreshAttemptAt; - ConnectorOnlineStatus onlineStatus; - Integer numDataOffers; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java deleted file mode 100644 index f597ac124..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/DataOfferDetailPageQueryService.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryContractOfferFetcher; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.DataOfferDetailRs; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -@RequiredArgsConstructor -public class DataOfferDetailPageQueryService { - private final CatalogQueryContractOfferFetcher catalogQueryContractOfferFetcher; - private final BrokerServerSettings brokerServerSettings; - - public DataOfferDetailRs queryDataOfferDetailsPage(DSLContext dsl, String assetId, String endpoint) { - // We are re-using the catalog page query stuff as long as we can get away with it - var fields = new CatalogQueryFields( - Tables.CONNECTOR, - Tables.DATA_OFFER, - Tables.DATA_OFFER_VIEW_COUNT, - brokerServerSettings.getDataSpaceConfig() - ); - - var d = fields.getDataOfferTable(); - var c = fields.getConnectorTable(); - - return dsl.select( - d.ASSET_ID, - d.ASSET_JSON_LD.cast(String.class).as("assetJsonLd"), - d.CREATED_AT, - d.UPDATED_AT, - catalogQueryContractOfferFetcher.getContractOffers(fields.getDataOfferTable()).as("contractOffers"), - fields.getOfflineSinceOrLastUpdatedAt().as("connectorOfflineSinceOrLastUpdatedAt"), - c.ENDPOINT.as("connectorEndpoint"), - c.ONLINE_STATUS.as("connectorOnlineStatus"), - c.PARTICIPANT_ID.as("connectorParticipantId"), - fields.getOrganizationName().as("organizationName"), - fields.getViewCount().as("viewCount")) - .from(d) - .leftJoin(c).on(c.ENDPOINT.eq(d.CONNECTOR_ENDPOINT)) - .where(d.ASSET_ID.eq(assetId).and(d.CONNECTOR_ENDPOINT.eq(endpoint))) - .fetchOneInto(DataOfferDetailRs.class); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java deleted file mode 100644 index 377abeafa..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/ViewCountLogger.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer; - -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import org.jooq.DSLContext; - -import java.time.OffsetDateTime; - -public class ViewCountLogger { - public void increaseDataOfferViewCount(DSLContext dsl, String assetId, String endpoint) { - var v = Tables.DATA_OFFER_VIEW_COUNT; - dsl.insertInto(v, v.ASSET_ID, v.CONNECTOR_ENDPOINT, v.DATE).values(assetId, endpoint, OffsetDateTime.now()).execute(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java deleted file mode 100644 index f10d1dd1e..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/ContractOfferRs.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; - -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ContractOfferRs { - String contractOfferId; - String policyJson; - OffsetDateTime createdAt; - OffsetDateTime updatedAt; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java deleted file mode 100644 index 0b575849c..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/pages/dataoffer/model/DataOfferDetailRs.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model; - -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.time.OffsetDateTime; -import java.util.List; - -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class DataOfferDetailRs { - String assetId; - String assetJsonLd; - OffsetDateTime createdAt; - OffsetDateTime updatedAt; - List contractOffers; - String connectorEndpoint; - ConnectorOnlineStatus connectorOnlineStatus; - String connectorParticipantId; - String organizationName; - OffsetDateTime connectorOfflineSinceOrLastUpdatedAt; - Integer viewCount; -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java deleted file mode 100644 index 1c3f4e2f9..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonDeserializationUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.utils; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; - -import java.util.List; - -/** - * Some things are easier to fetch as json into a string with JooQ. - * In that case we need to deserialize that string into an object of our choice afterwards. - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class JsonDeserializationUtils { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final TypeReference>> TYPE_STRING_LIST_2 = new TypeReference<>() { - }; - - @SneakyThrows - public static List> read2dStringList(String json) { - return OBJECT_MAPPER.readValue(json, TYPE_STRING_LIST_2); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java deleted file mode 100644 index c3823b04a..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.utils; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.jooq.Condition; -import org.jooq.Field; -import org.jooq.impl.DSL; - -/** - * Utilities for dealing with PostgreSQL Like Operation values - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class LikeUtils { - - /** - * Create LIKE condition value for "field contains word". - * - * @param field field - * @param lowercaseWord word - * @return "%escapedWord%" - */ - public static Condition contains(Field field, String lowercaseWord) { - if (StringUtils.isBlank(lowercaseWord)) { - return DSL.trueCondition(); - } - - return field.likeIgnoreCase("%" + escape(lowercaseWord) + "%"); - } - - - /** - * Escapes "\", "%", "_" in given string for a LIKE operation - * - * @param string unescaped string - * @return escaped string - */ - public static String escape(String string) { - return string.replace("\\", "\\\\") - .replace("%", "\\%") - .replace("_", "\\_"); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java deleted file mode 100644 index 9cdcdd84d..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/MultisetUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.utils; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.jooq.Field; -import org.jooq.TableLike; -import org.jooq.impl.DSL; - -import java.util.List; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class MultisetUtils { - public static Field> multiset(TableLike table, Class type) { - return DSL.multiset(table).convertFrom(it -> it.into(type)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java deleted file mode 100644 index e8a6ef310..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/SearchUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.utils; - -import de.sovity.edc.ext.brokerserver.utils.StringUtils2; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.jooq.Condition; -import org.jooq.Field; -import org.jooq.impl.DSL; - -import java.util.List; - -/** - * DB Search Queries - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class SearchUtils { - - /** - * Simple search - *
      - * All search query words must be contained in at least one search target. - * - * @param searchQuery search query - * @param searchTargets target fields - * @return JOOQ Condition - */ - public static Condition simpleSearch(String searchQuery, List> searchTargets) { - var words = StringUtils2.lowercaseWords(searchQuery); - return DSL.and(words.stream() - .map(word -> anySearchTargetContains(searchTargets, word)) - .toList()); - } - - private static Condition anySearchTargetContains(List> searchTargets, String word) { - return DSL.or(searchTargets.stream().map(field -> LikeUtils.contains(field, word)).toList()); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java deleted file mode 100644 index b21cc2787..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCleaner.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import org.jooq.DSLContext; - -import java.util.Collection; - -public class ConnectorCleaner { - public void removeDataForDeadConnectors(DSLContext dsl, Collection endpoints) { - var doco = Tables.CONTRACT_OFFER; - var dof = Tables.DATA_OFFER; - dsl.deleteFrom(doco).where(PostgresqlUtils.in(doco.CONNECTOR_ENDPOINT, endpoints)).execute(); - dsl.deleteFrom(dof).where(PostgresqlUtils.in(dof.CONNECTOR_ENDPOINT, endpoints)).execute(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java deleted file mode 100644 index 51b5f461e..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorCreator.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; - -import java.time.OffsetDateTime; -import java.util.Collection; -import java.util.List; - -@RequiredArgsConstructor -public class ConnectorCreator { - private final ConnectorQueries connectorQueries; - - public void addConnector(DSLContext dsl, String connectorEndpoint) { - addConnectors(dsl, List.of(connectorEndpoint)); - } - - public void addConnectors(DSLContext dsl, Collection connectorEndpoints) { - // Don't create connectors that already exist - var existingConnectors = connectorQueries.findExistingConnectors(dsl, connectorEndpoints); - var newConnectors = CollectionUtils2.difference(connectorEndpoints, existingConnectors); - - var connectorRecords = newConnectors.stream() - .map(String::trim) - .map(this::newConnectorRow) - .toList(); - - if (!connectorRecords.isEmpty()) { - dsl.batchStore(connectorRecords).execute(); - } - } - - @NotNull - private ConnectorRecord newConnectorRow(String endpoint) { - var connector = new ConnectorRecord(); - connector.setEndpoint(endpoint); - connector.setParticipantId(""); - connector.setCreatedAt(OffsetDateTime.now()); - connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - return connector; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java deleted file mode 100644 index f44353fdb..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/ConnectorKiller.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import org.jooq.DSLContext; - -import java.util.Collection; - -public class ConnectorKiller { - public void killConnectors(DSLContext dsl, Collection endpoints) { - var c = Tables.CONNECTOR; - dsl.update(c).set(c.ONLINE_STATUS, ConnectorOnlineStatus.DEAD).where(PostgresqlUtils.in(c.ENDPOINT, endpoints)).execute(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java deleted file mode 100644 index 117bdfbe8..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/KnownConnectorsInitializer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.eclipse.edc.spi.system.configuration.Config; -import org.jooq.DSLContext; - -import java.util.Arrays; -import java.util.List; - -@RequiredArgsConstructor -public class KnownConnectorsInitializer { - private final Config config; - private final ConnectorQueue connectorQueue; - private final ConnectorCreator connectorCreator; - - public void addKnownConnectorsOnStartup(DSLContext dsl) { - var connectorEndpoints = getKnownConnectorsConfigValue(); - connectorCreator.addConnectors(dsl, connectorEndpoints); - connectorQueue.addAll(connectorEndpoints, ConnectorRefreshPriority.ADDED_ON_STARTUP); - } - - private List getKnownConnectorsConfigValue() { - var knownConnectorsString = config.getString(BrokerServerExtension.KNOWN_CONNECTORS, ""); - return Arrays.stream(knownConnectorsString.split(",")).map(String::trim).filter(StringUtils::isNotBlank).distinct().toList(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java deleted file mode 100644 index 8b46d9c81..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/OfflineConnectorKiller.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services; - -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -@RequiredArgsConstructor -public class OfflineConnectorKiller { - private final BrokerServerSettings brokerServerSettings; - private final ConnectorQueries connectorQueries; - private final BrokerEventLogger brokerEventLogger; - private final ConnectorKiller connectorKiller; - private final ConnectorCleaner connectorClearer; - - public void killIfOfflineTooLong(DSLContext dsl) { - var killOfflineConnectorsAfter = brokerServerSettings.getKillOfflineConnectorsAfter(); - var toKill = connectorQueries.findAllConnectorsForKilling(dsl, killOfflineConnectorsAfter); - - connectorClearer.removeDataForDeadConnectors(dsl, toKill); - connectorKiller.killConnectors(dsl, toKill); - - brokerEventLogger.addKilledDueToOfflineTooLongMessages(dsl, toKill); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java deleted file mode 100644 index 2cb4fdf2c..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AssetPropertyParser.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; - -import java.util.Map; - -@RequiredArgsConstructor -public class AssetPropertyParser { - private final ObjectMapper objectMapper; - - private final TypeReference> typeToken = new TypeReference<>() { - }; - - @SneakyThrows - public Map parsePropertiesFromJsonString(String assetPropertiesJson) { - return objectMapper.readValue(assetPropertiesJson, typeToken); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java deleted file mode 100644 index baf08bc02..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorDataOfferApiService.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferInfo; -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorDataOfferDetails; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.List; - -@RequiredArgsConstructor -public class AuthorityPortalConnectorDataOfferApiService { - private final AuthorityPortalConnectorQueryService authorityPortalConnectorQueryService; - private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; - - public List getConnectorDataOffersByEndpoints(DSLContext dsl, List endpoints) { - return authorityPortalConnectorQueryService.getConnectorsDataOffers(dsl, endpoints).stream() - .map(it -> new AuthorityPortalConnectorDataOfferInfo( - it.getConnectorEndpoint(), - it.getParticipantId(), - connectorOnlineStatusMapper.getOnlineStatus(it.getOnlineStatus()), - it.getOfflineSinceOrLastUpdatedAt(), - it.getDataOffers().stream().map(dataOffer -> new AuthorityPortalConnectorDataOfferDetails(dataOffer.getDataOfferId(), dataOffer.getDataOfferName())).toList() - )) - .toList(); - } - -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java deleted file mode 100644 index 60d103bef..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiService.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalConnectorInfo; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.List; - -@RequiredArgsConstructor -public class AuthorityPortalConnectorMetadataApiService { - private final AuthorityPortalConnectorQueryService authorityPortalConnectorQueryService; - private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; - - public List getMetadataByEndpoints(DSLContext dsl, List endpoints) { - - return authorityPortalConnectorQueryService.getConnectorMetadata(dsl, endpoints).stream() - .map(it -> new AuthorityPortalConnectorInfo( - it.getConnectorEndpoint(), - it.getParticipantId(), - it.getDataOfferCount(), - connectorOnlineStatusMapper.getOnlineStatus(it.getOnlineStatus()), - it.getOfflineSinceOrLastUpdatedAt() - )) - .toList(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java deleted file mode 100644 index bb432ae5c..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorQueryService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import lombok.AccessLevel; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.experimental.FieldDefaults; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; -import org.jooq.Field; -import org.jooq.impl.DSL; - -import java.time.OffsetDateTime; -import java.util.List; - -import static org.jooq.impl.DSL.coalesce; -import static org.jooq.impl.DSL.count; -import static org.jooq.impl.DSL.select; - -@RequiredArgsConstructor -public class AuthorityPortalConnectorQueryService { - - @Data - @FieldDefaults(level = AccessLevel.PRIVATE) - public static class ConnectorMetadataRs { - String connectorEndpoint; - String participantId; - ConnectorOnlineStatus onlineStatus; - OffsetDateTime offlineSinceOrLastUpdatedAt; - Integer dataOfferCount; - } - - @Data - @FieldDefaults(level = AccessLevel.PRIVATE) - public static class ConnectorDetailsRs { - String connectorEndpoint; - String participantId; - ConnectorOnlineStatus onlineStatus; - OffsetDateTime offlineSinceOrLastUpdatedAt; - List dataOffers; - } - - @Data - @FieldDefaults(level = AccessLevel.PRIVATE) - public static class DataOfferRs { - String dataOfferId; - String dataOfferName; - } - - @NotNull - public List getConnectorMetadata(DSLContext dsl, List endpoints) { - var c = Tables.CONNECTOR; - - return dsl.select( - c.ENDPOINT.as("connectorEndpoint"), - c.PARTICIPANT_ID.as("participantId"), - c.ONLINE_STATUS.as("onlineStatus"), - CatalogQueryFields.offlineSinceOrLastUpdatedAt(c).as("offlineSinceOrLastUpdatedAt"), - getDataOfferCount(c.ENDPOINT).as("dataOfferCount") - ) - .from(c) - .where(PostgresqlUtils.in(c.ENDPOINT, endpoints)) - .fetchInto(ConnectorMetadataRs.class); - } - - @NotNull - public Field getDataOfferCount(Field connectorEndpoint) { - var d = Tables.DATA_OFFER; - - return select(coalesce(count().cast(Integer.class), DSL.value(0))) - .from(d) - .where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)) - .asField(); - } - - @NotNull - public List getDataOffers(DSLContext dsl, String connectorEndpoint) { - var d = Tables.DATA_OFFER; - - return dsl.select( - d.ASSET_TITLE.as("dataOfferName"), - d.ASSET_ID.as("dataOfferId") - ) - .from(d) - .where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)) - .fetchInto(DataOfferRs.class); - } - - - @NotNull - public List getConnectorsDataOffers(DSLContext dsl, List endpoints) { - var c = Tables.CONNECTOR; - - var connectors = dsl.select( - c.ENDPOINT.as("connectorEndpoint"), - c.PARTICIPANT_ID.as("participantId"), - c.ONLINE_STATUS.as("onlineStatus"), - CatalogQueryFields.offlineSinceOrLastUpdatedAt(c).as("offlineSinceOrLastUpdatedAt") - ) - .from(c) - .where(PostgresqlUtils.in(c.ENDPOINT, endpoints)) - .fetchInto(ConnectorDetailsRs.class); - connectors.forEach(connector -> connector.dataOffers = getDataOffers(dsl, connector.connectorEndpoint)); - return connectors; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java deleted file mode 100644 index 84086463b..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiService.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.AuthorityPortalOrganizationMetadata; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.OrganizationMetadataRecord; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; - -import java.util.List; - -@RequiredArgsConstructor -public class AuthorityPortalOrganizationMetadataApiService { - - public void setOrganizationMetadata(DSLContext dsl, List organizationMetadata) { - var records = organizationMetadata.stream().map(this::buildRecord).toList(); - - dsl.deleteFrom(Tables.ORGANIZATION_METADATA).execute(); - dsl.batchInsert(records).execute(); - } - - @NotNull - private OrganizationMetadataRecord buildRecord(AuthorityPortalOrganizationMetadata it) { - var record = new OrganizationMetadataRecord(); - record.setMdsId(it.getMdsId()); - record.setName(it.getName()); - - return record; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java deleted file mode 100644 index d83d1cef5..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiService.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.CatalogContractOffer; -import de.sovity.edc.ext.brokerserver.api.model.CatalogDataOffer; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageResult; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingItem; -import de.sovity.edc.ext.brokerserver.api.model.CatalogPageSortingType; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.DataOfferListEntryRs; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; -import de.sovity.edc.ext.brokerserver.services.api.filtering.CatalogFilterService; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Stream; - -@RequiredArgsConstructor -public class CatalogApiService { - private final PaginationMetadataUtils paginationMetadataUtils; - private final CatalogQueryService catalogQueryService; - private final DataOfferMappingUtils dataOfferMappingUtils; - private final CatalogFilterService catalogFilterService; - private final BrokerServerSettings brokerServerSettings; - - public CatalogPageResult catalogPage(DSLContext dsl, CatalogPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - - - var filters = catalogFilterService.getCatalogQueryFilters(query.getFilter()); - - var pageQuery = paginationMetadataUtils.getPageQuery( - query.getPageOneBased(), - brokerServerSettings.getCatalogPagePageSize() - ); - - var availableSortings = buildAvailableSortings(); - var sorting = query.getSorting(); - if (sorting == null) { - sorting = availableSortings.get(0).getSorting(); - } - - // execute db query - var catalogPageRs = catalogQueryService.queryCatalogPage( - dsl, - query.getSearchQuery(), - filters, - sorting, - pageQuery - ); - - var paginationMetadata = paginationMetadataUtils.buildPaginationMetadata( - query.getPageOneBased(), - brokerServerSettings.getCatalogPagePageSize(), - catalogPageRs.getDataOffers().size(), - catalogPageRs.getNumTotalDataOffers() - ); - - var result = new CatalogPageResult(); - result.setAvailableSortings(availableSortings); - result.setPaginationMetadata(paginationMetadata); - result.setAvailableFilters(catalogFilterService.buildAvailableFilters(catalogPageRs.getAvailableFilterValues())); - result.setDataOffers(buildCatalogDataOffers(catalogPageRs.getDataOffers())); - return result; - } - - private List buildCatalogDataOffers(List dataOfferRs) { - return dataOfferRs.stream() - .map(this::buildCatalogDataOffer) - .toList(); - } - - private CatalogDataOffer buildCatalogDataOffer(DataOfferListEntryRs dataOfferRs) { - var asset = dataOfferMappingUtils.buildUiAsset( - dataOfferRs.getAssetJsonLd(), - dataOfferRs.getConnectorEndpoint(), - dataOfferRs.getConnectorParticipantId(), - dataOfferRs.getOrganizationName() - ); - - var dataOffer = new CatalogDataOffer(); - dataOffer.setAssetId(dataOfferRs.getAssetId()); - dataOffer.setCreatedAt(dataOfferRs.getCreatedAt()); - dataOffer.setUpdatedAt(dataOfferRs.getUpdatedAt()); - dataOffer.setAsset(asset); - dataOffer.setContractOffers(buildCatalogContractOffers(dataOfferRs)); - dataOffer.setConnectorEndpoint(dataOfferRs.getConnectorEndpoint()); - dataOffer.setConnectorOfflineSinceOrLastUpdatedAt(dataOfferRs.getConnectorOfflineSinceOrLastUpdatedAt()); - dataOffer.setConnectorOnlineStatus(getOnlineStatus(dataOfferRs)); - return dataOffer; - } - - private List buildCatalogContractOffers(DataOfferListEntryRs dataOfferRs) { - return dataOfferRs.getContractOffers().stream() - .map(this::buildCatalogContractOffer) - .toList(); - } - - private CatalogContractOffer buildCatalogContractOffer(ContractOfferRs contractOfferDbRow) { - var contractOffer = new CatalogContractOffer(); - contractOffer.setContractOfferId(contractOfferDbRow.getContractOfferId()); - contractOffer.setContractPolicy(dataOfferMappingUtils.buildUiPolicy(contractOfferDbRow.getPolicyJson())); - contractOffer.setCreatedAt(contractOfferDbRow.getCreatedAt()); - contractOffer.setUpdatedAt(contractOfferDbRow.getUpdatedAt()); - return contractOffer; - } - - private ConnectorOnlineStatus getOnlineStatus(DataOfferListEntryRs dataOfferRs) { - return switch (dataOfferRs.getConnectorOnlineStatus()) { - case ONLINE -> ConnectorOnlineStatus.ONLINE; - case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - case DEAD -> ConnectorOnlineStatus.DEAD; - default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + dataOfferRs.getConnectorOnlineStatus()); - }; - } - - private static List buildAvailableSortings() { - return Stream.of( - CatalogPageSortingType.MOST_RECENT, - CatalogPageSortingType.TITLE, - CatalogPageSortingType.ORIGINATOR, - CatalogPageSortingType.VIEW_COUNT - ).map(it -> new CatalogPageSortingItem(it, it.getTitle())).toList(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java deleted file mode 100644 index ffa3c9c5e..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiService.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorCreationRequest; -import de.sovity.edc.ext.brokerserver.api.model.AddedConnector; -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import de.sovity.edc.ext.brokerserver.utils.MdsIdUtils; -import de.sovity.edc.ext.brokerserver.utils.UrlUtils; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.List; -import java.util.Objects; - -import static de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority.ADDED_ON_API_CALL; -import static java.util.stream.Collectors.toSet; - -@RequiredArgsConstructor -public class ConnectorApiService { - private final ConnectorService connectorService; - private final BrokerEventLogger brokerEventLogger; - private final ConnectorQueries connectorQueries; - - - public void addConnectors(DSLContext dsl, List connectorEndpoints) { - var existingEndpoints = connectorService.getConnectorEndpoints(dsl); - var endpoints = connectorEndpoints.stream() - .filter(Objects::nonNull) - .map(String::trim) - .filter(UrlUtils::isValidUrl) - .filter(endpoint -> !existingEndpoints.contains(endpoint)) - .collect(toSet()); - connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); - } - - public void addConnectorsWithMdsIds(DSLContext dsl, ConnectorCreationRequest connectorCreationRequests) { - var connectors = connectorCreationRequests.getConnectors(); - var existingEndpoints = connectorService.getConnectorEndpoints(dsl); - - connectors.removeIf(it -> it.getConnectorEndpoint() == null || it.getMdsId() == null); - connectors.forEach(it -> { - it.setConnectorEndpoint(it.getConnectorEndpoint().trim()); - it.setMdsId(it.getMdsId().trim()); - }); - connectors.removeIf(it -> - !UrlUtils.isValidUrl(it.getConnectorEndpoint()) - || !MdsIdUtils.isValidMdsId(it.getMdsId()) - || existingEndpoints.contains(it.getConnectorEndpoint()) - ); - - var endpoints = connectors.stream().map(AddedConnector::getConnectorEndpoint).collect(toSet()); - connectorService.addConnectors(dsl, endpoints, ADDED_ON_API_CALL); - addMdsIdsToConnectors(dsl, connectors); - } - - public void deleteConnectors(DSLContext dsl, List connectorEndpoints) { - connectorService.deleteConnectors(dsl, connectorEndpoints); - brokerEventLogger.logConnectorsDeleted(dsl, connectorEndpoints); - } - - private void addMdsIdsToConnectors(DSLContext dsl, List connectors) { - connectors.forEach(it -> { - var connector = connectorQueries.findByEndpoint(dsl, it.getConnectorEndpoint()); - if (connector != null) { - connector.setMdsId(it.getMdsId()); - connector.update(); - } - }); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java deleted file mode 100644 index f44097b6e..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorDetailApiService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorDetailPageResult; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorDetailQueryService; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.Objects; - -@RequiredArgsConstructor -public class ConnectorDetailApiService { - private final ConnectorDetailQueryService connectorDetailQueryService; - private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; - - public ConnectorDetailPageResult connectorDetailPage(DSLContext dsl, ConnectorDetailPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - - var connectorDbRow = connectorDetailQueryService.queryConnectorDetailPage(dsl, query.getConnectorEndpoint()); - var dto = new ConnectorDetailPageResult(); - dto.setParticipantId(connectorDbRow.getParticipantId()); - dto.setEndpoint(connectorDbRow.getEndpoint()); - dto.setOrganizationName(connectorDbRow.getOrganizationName()); - dto.setCreatedAt(connectorDbRow.getCreatedAt()); - dto.setLastRefreshAttemptAt(connectorDbRow.getLastRefreshAttemptAt()); - dto.setLastSuccessfulRefreshAt(connectorDbRow.getLastSuccessfulRefreshAt()); - dto.setOnlineStatus(connectorOnlineStatusMapper.getOnlineStatus(connectorDbRow.getOnlineStatus())); - dto.setNumDataOffers(connectorDbRow.getNumDataOffers()); - dto.setConnectorCrawlingTimeAvg(connectorDbRow.getConnectorCrawlingTimeAvg()); - return dto; - } - -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java deleted file mode 100644 index ca0a8d965..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorListApiService.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorListEntry; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageResult; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingItem; -import de.sovity.edc.ext.brokerserver.api.model.ConnectorPageSortingType; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.ConnectorListQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.connector.model.ConnectorListEntryRs; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Stream; - -@RequiredArgsConstructor -public class ConnectorListApiService { - private final ConnectorListQueryService connectorListQueryService; - private final ConnectorOnlineStatusMapper connectorOnlineStatusMapper; - private final PaginationMetadataUtils paginationMetadataUtils; - - public ConnectorPageResult connectorListPage(DSLContext dsl, ConnectorPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - - var availableSortings = buildAvailableSortings(); - var sorting = query.getSorting(); - if (sorting == null) { - sorting = availableSortings.get(0).getSorting(); - } - - var connectorDbRows = connectorListQueryService.queryConnectorPage(dsl, query.getSearchQuery(), sorting); - - var result = new ConnectorPageResult(); - result.setAvailableSortings(availableSortings); - result.setPaginationMetadata(paginationMetadataUtils.buildDummyPaginationMetadata(connectorDbRows.size())); - result.setConnectors(buildConnectorListEntries(connectorDbRows)); - return result; - } - - private List buildConnectorListEntries(List connectors) { - return connectors.stream().map(this::buildConnectorListEntry).toList(); - } - - private ConnectorListEntry buildConnectorListEntry(ConnectorListEntryRs connector) { - var dto = new ConnectorListEntry(); - dto.setParticipantId(connector.getParticipantId()); - dto.setEndpoint(connector.getEndpoint()); - dto.setOrganizationName(connector.getOrganizationName()); - dto.setCreatedAt(connector.getCreatedAt()); - dto.setLastRefreshAttemptAt(connector.getLastRefreshAttemptAt()); - dto.setLastSuccessfulRefreshAt(connector.getLastSuccessfulRefreshAt()); - dto.setOnlineStatus(connectorOnlineStatusMapper.getOnlineStatus(connector.getOnlineStatus())); - dto.setNumDataOffers(connector.getNumDataOffers()); - return dto; - } - - private List buildAvailableSortings() { - return Stream.of( - ConnectorPageSortingType.MOST_RECENT, - ConnectorPageSortingType.TITLE - ).map(it -> new ConnectorPageSortingItem(it, it.getTitle())).toList(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java deleted file mode 100644 index 15f204711..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorOnlineStatusMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class ConnectorOnlineStatusMapper { - - public ConnectorOnlineStatus getOnlineStatus(de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus onlineStatus) { - return switch (onlineStatus) { - case ONLINE -> ConnectorOnlineStatus.ONLINE; - case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - case DEAD -> ConnectorOnlineStatus.DEAD; - default -> throw new IllegalStateException("Unknown ConnectorOnlineStatus from DAO for API: " + onlineStatus); - }; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java deleted file mode 100644 index b3996dcb8..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorService.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueue; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; -import org.jooq.Record; -import org.jooq.TableField; - -import java.util.Collection; -import java.util.Set; - -@RequiredArgsConstructor -public class ConnectorService { - private final ConnectorCreator connectorCreator; - private final ConnectorQueue connectorQueue; - - public void addConnectors(DSLContext dsl, Collection connectorEndpoints, int priority) { - connectorCreator.addConnectors(dsl, connectorEndpoints); - connectorQueue.addAll(connectorEndpoints, priority); - } - - public void deleteConnectors(DSLContext dsl, Collection endpoints) { - removeConnectorRows(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, endpoints); - removeConnectorRows(dsl, Tables.CONTRACT_OFFER.CONNECTOR_ENDPOINT, endpoints); - removeConnectorRows(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, endpoints); - removeConnectorRows(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, endpoints); - removeConnectorRows(dsl, Tables.CONNECTOR.ENDPOINT, endpoints); - } - - public Set getConnectorEndpoints(DSLContext dsl) { - return dsl.select(Tables.CONNECTOR.ENDPOINT).from(Tables.CONNECTOR).fetchSet(Tables.CONNECTOR.ENDPOINT); - } - - private void removeConnectorRows( - DSLContext dsl, - TableField endpointField, - Collection endpoints - ) { - dsl.deleteFrom(endpointField.getTable()).where(endpointField.in(endpoints)).execute(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java deleted file mode 100644 index a79bd8b96..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiService.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailContractOffer; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.brokerserver.api.model.DataOfferDetailPageResult; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.DataOfferDetailPageQueryService; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.ViewCountLogger; -import de.sovity.edc.ext.brokerserver.dao.pages.dataoffer.model.ContractOfferRs; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; - -import java.util.List; -import java.util.Objects; - -@RequiredArgsConstructor -public class DataOfferDetailApiService { - private final DataOfferDetailPageQueryService dataOfferDetailPageQueryService; - private final ViewCountLogger viewCountLogger; - private final DataOfferMappingUtils dataOfferMappingUtils; - - public DataOfferDetailPageResult dataOfferDetailPage(DSLContext dsl, DataOfferDetailPageQuery query) { - Objects.requireNonNull(query, "query must not be null"); - - var dataOffer = dataOfferDetailPageQueryService.queryDataOfferDetailsPage(dsl, query.getAssetId(), query.getConnectorEndpoint()); - var asset = dataOfferMappingUtils.buildUiAsset( - dataOffer.getAssetJsonLd(), - dataOffer.getConnectorEndpoint(), - dataOffer.getConnectorParticipantId(), - dataOffer.getOrganizationName() - ); - viewCountLogger.increaseDataOfferViewCount(dsl, query.getAssetId(), query.getConnectorEndpoint()); - - var result = new DataOfferDetailPageResult(); - result.setAssetId(dataOffer.getAssetId()); - result.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); - result.setConnectorOnlineStatus(mapConnectorOnlineStatus(dataOffer.getConnectorOnlineStatus())); - result.setConnectorOfflineSinceOrLastUpdatedAt(dataOffer.getConnectorOfflineSinceOrLastUpdatedAt()); - result.setAsset(asset); - result.setCreatedAt(dataOffer.getCreatedAt()); - result.setUpdatedAt(dataOffer.getUpdatedAt()); - result.setContractOffers(buildDataOfferDetailContractOffers(dataOffer.getContractOffers())); - result.setViewCount(dataOffer.getViewCount()); - return result; - } - - private ConnectorOnlineStatus mapConnectorOnlineStatus( - de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus connectorOnlineStatus - ) { - if (connectorOnlineStatus == null) { - return ConnectorOnlineStatus.OFFLINE; - } - - return switch (connectorOnlineStatus) { - case ONLINE -> ConnectorOnlineStatus.ONLINE; - case OFFLINE -> ConnectorOnlineStatus.OFFLINE; - case DEAD -> ConnectorOnlineStatus.DEAD; - }; - } - - private List buildDataOfferDetailContractOffers(List contractOffers) { - return contractOffers.stream().map(this::buildDataOfferDetailContractOffer).toList(); - } - - @NotNull - private DataOfferDetailContractOffer buildDataOfferDetailContractOffer(ContractOfferRs offer) { - var newOffer = new DataOfferDetailContractOffer(); - newOffer.setCreatedAt(offer.getCreatedAt()); - newOffer.setUpdatedAt(offer.getUpdatedAt()); - newOffer.setContractOfferId(offer.getContractOfferId()); - newOffer.setContractPolicy(dataOfferMappingUtils.buildUiPolicy(offer.getPolicyJson())); - return newOffer; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java deleted file mode 100644 index 77f0cd8f4..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferMappingUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; -import de.sovity.edc.utils.JsonUtils; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class DataOfferMappingUtils { - private final PolicyMapper policyMapper; - private final AssetMapper assetMapper; - - public UiAsset buildUiAsset(String assetJsonLd, String endpoint, String participantId, String organizationName) { - var asset = assetMapper.buildAsset(JsonUtils.parseJsonObj(assetJsonLd)); - var uiAsset = assetMapper.buildUiAsset(asset, endpoint, participantId); - uiAsset.setCreatorOrganizationName(organizationName); - return uiAsset; - } - - public UiPolicy buildUiPolicy(String policyJson) { - var policy = policyMapper.buildPolicy(policyJson); - return policyMapper.buildUiPolicy(policy); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java deleted file mode 100644 index d69cbb707..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/PaginationMetadataUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.api.model.PaginationMetadata; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.PageQuery; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; - -@RequiredArgsConstructor -public class PaginationMetadataUtils { - - @NotNull - public PaginationMetadata buildDummyPaginationMetadata(int numResults) { - return new PaginationMetadata(numResults, numResults, 1, numResults); - } - - public PageQuery getPageQuery(Integer pageOneBased, int pageSize) { - int pageZeroBased = getPageZeroBased(pageOneBased); - int offset = pageZeroBased * pageSize; - return new PageQuery(offset, pageSize); - } - - public PaginationMetadata buildPaginationMetadata(Integer pageOneBased, int pageSize, int numVisible, int numTotalResults) { - int pageZeroBased = getPageZeroBased(pageOneBased); - var paginationMetadata = new PaginationMetadata(); - paginationMetadata.setNumTotal(numTotalResults); - paginationMetadata.setNumVisible(numVisible); - paginationMetadata.setPageOneBased(pageZeroBased + 1); - paginationMetadata.setPageSize(pageSize); - return paginationMetadata; - } - - private int getPageZeroBased(Integer pageOneBased) { - return pageOneBased == null ? 0 : (pageOneBased - 1); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java deleted file mode 100644 index 18c9218ba..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/AttributeFilterQuery.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api.filtering; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import org.jooq.Condition; - -import java.util.Collection; - -@FunctionalInterface -public interface AttributeFilterQuery { - - /** - * Filters a Catalog DB Query for a given Filter Attribute with selected values - * - * @param fields available tables and fields during the catalog query - * @param values values to be filtered by. Usually this should mean that only one of the values needs to be present. - * @return {@link Condition} - */ - Condition filterDataOffers(CatalogQueryFields fields, Collection values); - -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java deleted file mode 100644 index 9d13a2c00..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinition.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api.filtering; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.AvailableFilterValuesQuery; - -/** - * Implementation of a filter attribute definition for the catalog. - * - * @param name technical id of the attribute - * @param label UI showing label for the attribute - * @param valueGetter query existing values from DB - * @param filterApplier apply a filter to a data offer query - */ -public record CatalogFilterAttributeDefinition( - String name, - String label, - AvailableFilterValuesQuery valueGetter, - AttributeFilterQuery filterApplier -) { -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java deleted file mode 100644 index 088b2cc84..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterAttributeDefinitionService.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api.filtering; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.utils.PostgresqlUtils; -import org.jooq.Field; - -import java.util.function.Function; - -public class CatalogFilterAttributeDefinitionService { - - public CatalogFilterAttributeDefinition forField( - Function> fieldExtractor, - String name, - String label - ) { - return new CatalogFilterAttributeDefinition( - name, - label, - fieldExtractor::apply, - (fields, values) -> PostgresqlUtils.in(fieldExtractor.apply(fields), values) - ); - } - - public CatalogFilterAttributeDefinition buildDataSpaceFilter() { - return new CatalogFilterAttributeDefinition( - "dataSpace", - "Data Space", - CatalogQueryFields::getDataSpace, - (fields, values) -> PostgresqlUtils.in(fields.getDataSpace(), values) - ); - } - - public CatalogFilterAttributeDefinition buildConnectorEndpointFilter() { - return new CatalogFilterAttributeDefinition( - "connectorEndpoint", - "Connector", - fields -> fields.getDataOfferTable().CONNECTOR_ENDPOINT, - (fields, values) -> PostgresqlUtils.in(fields.getDataOfferTable().CONNECTOR_ENDPOINT, values) - ); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java deleted file mode 100644 index 860a796ab..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogFilterService.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api.filtering; - -import de.sovity.edc.ext.brokerserver.api.model.CnfFilter; -import de.sovity.edc.ext.brokerserver.api.model.CnfFilterAttribute; -import de.sovity.edc.ext.brokerserver.api.model.CnfFilterItem; -import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValue; -import de.sovity.edc.ext.brokerserver.api.model.CnfFilterValueAttribute; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQueryFilter; -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.models.CatalogQuerySelectedFilterQuery; -import de.sovity.edc.ext.brokerserver.dao.utils.JsonDeserializationUtils; -import de.sovity.edc.ext.brokerserver.utils.CollectionUtils2; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.Validate; -import org.jooq.impl.DSL; - -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.toMap; - -@RequiredArgsConstructor -public class CatalogFilterService { - private final CatalogFilterAttributeDefinitionService catalogFilterAttributeDefinitionService; - - private final Comparator caseInsensitiveEmptyStringLast = (s1, s2) -> { - int result = s1.compareToIgnoreCase(s2); - if (s1.isEmpty() && !s2.isEmpty()) { - return 1; - } else if (!s1.isEmpty() && s2.isEmpty()) { - return -1; - } else { - return result; - } - }; - - - /** - * Currently supported filters for the catalog page. - * - * @return attribute definitions - */ - private List getAvailableFilters() { - return List.of( - catalogFilterAttributeDefinitionService.buildDataSpaceFilter(), - catalogFilterAttributeDefinitionService.forField( - fields -> fields.getDataOfferTable().DATA_CATEGORY, - "dataCategory", - "Data Category" - ), - catalogFilterAttributeDefinitionService.forField( - fields -> fields.getDataOfferTable().DATA_SUBCATEGORY, - "dataSubcategory", - "Data Subcategory" - ), - catalogFilterAttributeDefinitionService.forField( - fields -> fields.getDataOfferTable().DATA_MODEL, - "dataModel", - "Data Model" - ), - catalogFilterAttributeDefinitionService.forField( - fields -> fields.getDataOfferTable().TRANSPORT_MODE, - "transportMode", - "Transport Mode" - ), - catalogFilterAttributeDefinitionService.forField( - fields -> fields.getDataOfferTable().GEO_REFERENCE_METHOD, - "geoReferenceMethod", - "Geo Reference Method" - ), - catalogFilterAttributeDefinitionService.forField( - CatalogQueryFields::getOrganizationName, - "curatorOrganizationName", - "Organization Name" - ), - catalogFilterAttributeDefinitionService.forField( - fields -> DSL.coalesce(fields.getConnectorTable().MDS_ID, "Unknown"), - "curatorMdsId", - "MDS ID" - ), - catalogFilterAttributeDefinitionService.buildConnectorEndpointFilter() - ); - } - - public List getCatalogQueryFilters(CnfFilterValue cnfFilterValue) { - var values = getCnfFilterValuesMap(cnfFilterValue); - return getAvailableFilters().stream() - .map(filter -> new CatalogQueryFilter( - filter.name(), - filter.valueGetter(), - getQueryFilter(filter, values.get(filter.name())) - )) - .toList(); - } - - private CatalogQuerySelectedFilterQuery getQueryFilter(CatalogFilterAttributeDefinition filter, List values) { - if (CollectionUtils2.isNotEmpty(values)) { - return fields -> filter.filterApplier().filterDataOffers(fields, values); - } - return null; - } - - public CnfFilter buildAvailableFilters(String filterValuesJson) { - var filterValues = JsonDeserializationUtils.read2dStringList(filterValuesJson); - var filterAttributes = zipAvailableFilters(getAvailableFilters(), filterValues) - .map(availableFilter -> new CnfFilterAttribute( - availableFilter.definition().name(), - availableFilter.definition().label(), - buildAvailableFilterValues(availableFilter) - )) - .toList(); - return new CnfFilter(filterAttributes); - } - - private List buildAvailableFilterValues(AvailableFilter availableFilter) { - return availableFilter.availableValues().stream() - .sorted(caseInsensitiveEmptyStringLast) - .map(value -> new CnfFilterItem(value, value)) - .toList(); - } - - private Stream zipAvailableFilters(List availableFilters, List> filterValues) { - Validate.isTrue( - availableFilters.size() == filterValues.size(), - "Number of available filters and filter values must match: %d != %d", - availableFilters.size(), - filterValues.size() - ); - return Stream.iterate(0, i -> i + 1) - .limit(availableFilters.size()) - .map(i -> new AvailableFilter(availableFilters.get(i), filterValues.get(i))); - } - - private record AvailableFilter(CatalogFilterAttributeDefinition definition, List availableValues) { - } - - private Map> getCnfFilterValuesMap(CnfFilterValue cnfFilterValue) { - if (cnfFilterValue == null || cnfFilterValue.getSelectedAttributeValues() == null) { - return Map.of(); - } - return cnfFilterValue.getSelectedAttributeValues().stream() - .filter(it -> it.getId() != null && CollectionUtils2.isNotEmpty(it.getSelectedIds())) - .collect(toMap(CnfFilterValueAttribute::getId, CnfFilterValueAttribute::getSelectedIds)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java deleted file mode 100644 index b7abd8b6e..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/api/filtering/CatalogSearchService.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api.filtering; - -import de.sovity.edc.ext.brokerserver.dao.pages.catalog.CatalogQueryFields; -import de.sovity.edc.ext.brokerserver.dao.utils.SearchUtils; -import lombok.RequiredArgsConstructor; -import org.jooq.Condition; - -import java.util.List; - -@RequiredArgsConstructor -public class CatalogSearchService { - - public Condition filterBySearch(CatalogQueryFields fields, String searchQuery) { - return SearchUtils.simpleSearch(searchQuery, List.of( - fields.getDataOfferTable().ASSET_ID, - fields.getDataOfferTable().ASSET_TITLE, - fields.getDataOfferTable().DATA_CATEGORY, - fields.getDataOfferTable().DATA_SUBCATEGORY, - fields.getDataOfferTable().DESCRIPTION, - fields.getDataOfferTable().CURATOR_ORGANIZATION_NAME, - fields.getDataOfferTable().KEYWORDS_COMMA_JOINED, - fields.getConnectorTable().ENDPOINT, - fields.getConnectorTable().PARTICIPANT_ID, - fields.getOrganizationName() - )); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java deleted file mode 100644 index e5f9571b9..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/AdminApiKeyValidator.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.config; - -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Response; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class AdminApiKeyValidator { - private final BrokerServerSettings brokerServerSettings; - - public void validateAdminApiKey(String adminApiKey) { - if (!brokerServerSettings.getAdminApiKey().equals(adminApiKey)) { - throw new WebApplicationException("Invalid admin API key", Response.Status.UNAUTHORIZED); - } - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java deleted file mode 100644 index a09814341..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettingsFactory.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.config; - -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.system.configuration.Config; - -import java.time.Duration; -import java.util.Arrays; -import java.util.List; - -@RequiredArgsConstructor -public class BrokerServerSettingsFactory { - private final Config config; - private final Monitor monitor; - - public BrokerServerSettings buildBrokerServerSettings() { - var adminApiKey = Validate.notBlank(config.getString(BrokerServerExtension.ADMIN_API_KEY), - "Need to configure %s.".formatted(BrokerServerExtension.ADMIN_API_KEY)); - var hideOfflineDataOffersAfter = getDuration(BrokerServerExtension.HIDE_OFFLINE_DATA_OFFERS_AFTER, null); - var catalogPagePageSize = config.getInteger(BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, 20); - var dataSpaceConfig = buildDataSpaceConfig(config); - var numThreads = config.getInteger(BrokerServerExtension.NUM_THREADS, 1); - var killOfflineConnectorsAfter = getDuration(BrokerServerExtension.KILL_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); - var maxDataOffers = config.getInteger(BrokerServerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, -1); - var maxContractOffers = config.getInteger(BrokerServerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER, -1); - - return BrokerServerSettings.builder() - .adminApiKey(adminApiKey) - .hideOfflineDataOffersAfter(hideOfflineDataOffersAfter) - .catalogPagePageSize(catalogPagePageSize) - .dataSpaceConfig(dataSpaceConfig) - .numThreads(numThreads) - .killOfflineConnectorsAfter(killOfflineConnectorsAfter) - .maxDataOffersPerConnector(maxDataOffers) - .maxContractOffersPerDataOffer(maxContractOffers) - .build(); - } - - private DataSpaceConfig buildDataSpaceConfig(Config config) { - var dataSpaceConfig = new DataSpaceConfig(getKnownDataSpaceEndpoints(config), getDefaultDataSpace(config)); - monitor.info("Default Dataspace Name: %s".formatted(dataSpaceConfig.defaultDataSpace())); - dataSpaceConfig.dataSpaceConnectors().forEach(dataSpaceConnector -> monitor.info("Using Dataspace Name %s for %s." - .formatted(dataSpaceConnector.dataSpaceName(), dataSpaceConnector.endpoint()))); - if (dataSpaceConfig.dataSpaceConnectors().isEmpty()) { - monitor.info("No additional data space names configured."); - } - return dataSpaceConfig; - } - - private List getKnownDataSpaceEndpoints(Config config) { - // Example: "Example1=http://connector-endpoint1.org,Example2=http://connector-endpoint2.org" - var dataSpacesConfig = config.getString(BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, ""); - - return Arrays.stream(dataSpacesConfig.split(",")) - .map(String::trim) - .map(it -> it.split("=")) - .filter(it -> it.length == 2) - .map(it -> { - var dataSpaceName = it[0].trim(); - var dataSpaceEndpoint = it[1].trim(); - return new DataSpaceConnector(dataSpaceEndpoint, dataSpaceName); - }) - .filter(it -> StringUtils.isNotBlank(it.endpoint()) && StringUtils.isNotBlank(it.endpoint())) - .toList(); - } - - private String getDefaultDataSpace(Config config) { - return config.getString(BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "Default"); - } - - private Duration getDuration(@NonNull String configProperty, Duration defaultValue) { - var value = config.getString(configProperty, ""); - - if (StringUtils.isBlank(value)) { - return defaultValue; - } - - return Duration.parse(value); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java deleted file mode 100644 index 705a5c1cc..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.config; - -import java.util.List; - -public record DataSpaceConfig(List dataSpaceConnectors, String defaultDataSpace) { -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java deleted file mode 100644 index 589f08aa0..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/DataSpaceConnector.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.config; - -/** - * We have special connectors that represent entire other data spaces. - * Here we associate the name of the data space with the connector endpoint. - */ -public record DataSpaceConnector(String endpoint, String dataSpaceName) { -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java deleted file mode 100644 index 439e81be0..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLogger.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.logging; - -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.time.OffsetDateTime; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Updates a single connector. - */ -@RequiredArgsConstructor -public class BrokerEventLogger { - - public void logConnectorsDeleted(DSLContext dsl, Collection connectorEndpoints) { - var records = connectorEndpoints.stream().map(connectorEndpoint -> { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_DELETED); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage("Connector was deleted."); - return logEntry; - }).toList(); - dsl.batchInsert(records).execute(); - } - - public void logConnectorUpdated(DSLContext dsl, String connectorEndpoint, ConnectorChangeTracker changes) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage(changes.toString()); - logEntry.insert(); - } - - public void logConnectorOffline(DSLContext dsl, String connectorEndpoint, BrokerEventErrorMessage errorMessage) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE); - logEntry.setEventStatus(BrokerEventStatus.ERROR); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage("Connector is offline."); - logEntry.setErrorStack(errorMessage.stackTraceOrNull()); - logEntry.insert(); - } - - public void logConnectorOnline(DSLContext dsl, String connectorEndpoint) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_STATUS_CHANGE_ONLINE); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage("Connector is online."); - logEntry.insert(); - } - - public void logConnectorUpdateDataOfferLimitExceeded(DSLContext dsl, Integer maxDataOffersPerConnector, String endpoint) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector has more than %d data offers. Exceeding data offers will be ignored.".formatted(maxDataOffersPerConnector)); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.insert(); - } - - public void logConnectorUpdateDataOfferLimitOk(DSLContext dsl, String endpoint) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_DATA_OFFER_LIMIT_OK); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector is not exceeding the maximum number of data offers limit anymore."); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.insert(); - } - - public void logConnectorUpdateContractOfferLimitExceeded(DSLContext dsl, Integer maxContractOffersPerConnector, String endpoint) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Some data offers have more than %d contract offers. Exceeding contract offers will be ignored.: ".formatted(maxContractOffersPerConnector)); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.insert(); - } - - public void logConnectorUpdateContractOfferLimitOk(DSLContext dsl, String endpoint) { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_OK); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setConnectorEndpoint(endpoint); - logEntry.setUserMessage("Connector is not exceeding the maximum number of contract offers per data offer limit anymore."); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.insert(); - } - - public void addKilledDueToOfflineTooLongMessages(DSLContext dsl, List deletedConnectorEndpoints) { - var logEntries = deletedConnectorEndpoints.stream().map(endpoint -> { - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_KILLED_DUE_TO_OFFLINE_FOR_TOO_LONG); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setUserMessage("Connector was marked as dead for being offline too long."); - logEntry.setConnectorEndpoint(endpoint); - return logEntry; - }).collect(Collectors.toList()); - - dsl.batchInsert(logEntries).execute(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java deleted file mode 100644 index 7743ac753..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerExecutionTimeLogger.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.logging; - -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.time.OffsetDateTime; - -/** - * Updates a single connector. - */ -@RequiredArgsConstructor -public class BrokerExecutionTimeLogger { - public void logExecutionTime(DSLContext dsl, String connectorEndpoint, long executionTimeInMs, MeasurementErrorStatus errorStatus) { - var logEntry = dsl.newRecord(Tables.BROKER_EXECUTION_TIME_MEASUREMENT); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setDurationInMs(executionTimeInMs); - logEntry.setType(MeasurementType.CONNECTOR_REFRESH); - logEntry.setErrorStatus(errorStatus); - logEntry.setConnectorEndpoint(connectorEndpoint); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.insert(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java deleted file mode 100644 index a1d790494..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueue.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.queue; - -import de.sovity.edc.ext.brokerserver.services.refreshing.ConnectorUpdater; -import lombok.RequiredArgsConstructor; - -import java.util.ArrayList; -import java.util.Collection; - -@RequiredArgsConstructor -public class ConnectorQueue { - private final ConnectorUpdater connectorUpdater; - private final ThreadPool threadPool; - - /** - * Enqueues connectors for update. - * - * @param endpoints connector endpoints - * @param priority priority from {@link ConnectorRefreshPriority} - */ - public void addAll(Collection endpoints, int priority) { - var queuedConnectorEndpoints = threadPool.getQueuedConnectorEndpoints(); - endpoints = new ArrayList<>(endpoints); - endpoints.removeIf(queuedConnectorEndpoints::contains); - - for (String endpoint : endpoints) { - threadPool.enqueueConnectorRefreshTask(priority, () -> connectorUpdater.updateConnector(endpoint), endpoint); - } - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java deleted file mode 100644 index 51172fd21..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateSuccessWriter.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing; - -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferLimitsEnforcer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriter; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; -import de.sovity.edc.ext.brokerserver.utils.MdsIdUtils; -import lombok.RequiredArgsConstructor; -import org.jooq.DSLContext; - -import java.time.OffsetDateTime; -import java.util.Objects; - -@RequiredArgsConstructor -public class ConnectorUpdateSuccessWriter { - private final BrokerEventLogger brokerEventLogger; - private final DataOfferWriter dataOfferWriter; - private final DataOfferLimitsEnforcer dataOfferLimitsEnforcer; - - public void handleConnectorOnline( - DSLContext dsl, - ConnectorRecord connector, - FetchedCatalog catalog - ) { - // Limit data offers and log limitation if necessary - var limitedDataOffers = dataOfferLimitsEnforcer.enforceLimits(catalog.getDataOffers()); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, limitedDataOffers); - - // Log Status Change and set status to online if necessary - if (connector.getOnlineStatus() != ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { - brokerEventLogger.logConnectorOnline(dsl, connector.getEndpoint()); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - } - - // Track changes for final log message - var changes = new ConnectorChangeTracker(); - updateConnector(connector, catalog, changes); - - // Update data offers - dataOfferWriter.updateDataOffers(dsl, connector.getEndpoint(), limitedDataOffers.abbreviatedDataOffers(), changes); - - // Log event if changes are present - if (!changes.isEmpty()) { - brokerEventLogger.logConnectorUpdated(dsl, connector.getEndpoint(), changes); - } - } - - private static void updateConnector(ConnectorRecord connector, FetchedCatalog catalog, ConnectorChangeTracker changes) { - var now = OffsetDateTime.now(); - var participantId = catalog.getParticipantId(); - - connector.setLastSuccessfulRefreshAt(now); - connector.setLastRefreshAttemptAt(now); - if (!Objects.equals(connector.getParticipantId(), participantId)) { - connector.setParticipantId(participantId); - connector.setMdsId(MdsIdUtils.getMdsIdFromParticipantId(participantId)); - changes.setParticipantIdChanged(participantId); - } - connector.update(); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java deleted file mode 100644 index de9172f2a..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/exceptions/ConnectorUnreachableException.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.exceptions; - -public class ConnectorUnreachableException extends RuntimeException { - public ConnectorUnreachableException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java deleted file mode 100644 index 91cb0395f..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchApplier.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.DataOfferPatch; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.jooq.DSLContext; - -@RequiredArgsConstructor -public class DataOfferPatchApplier { - - @SneakyThrows - public void writeDataOfferPatch(DSLContext dsl, DataOfferPatch dataOfferPatch) { - if (!dataOfferPatch.getDataOffersToUpdate().isEmpty()) { - dsl.batchUpdate(dataOfferPatch.getDataOffersToUpdate()).execute(); - } - if (!dataOfferPatch.getContractOffersToUpdate().isEmpty()) { - dsl.batchUpdate(dataOfferPatch.getContractOffersToUpdate()).execute(); - } - - // insert: parent entity first - if (!dataOfferPatch.getDataOffersToInsert().isEmpty()) { - dsl.batchInsert(dataOfferPatch.getDataOffersToInsert()).execute(); - } - if (!dataOfferPatch.getContractOffersToInsert().isEmpty()) { - dsl.batchInsert(dataOfferPatch.getContractOffersToInsert()).execute(); - } - - // delete: child entity first - if (!dataOfferPatch.getContractOffersToDelete().isEmpty()) { - dsl.batchDelete(dataOfferPatch.getContractOffersToDelete()).execute(); - } - if (!dataOfferPatch.getDataOffersToDelete().isEmpty()) { - dsl.batchDelete(dataOfferPatch.getDataOffersToDelete()).execute(); - } - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java deleted file mode 100644 index 97b1e2102..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferRecordUpdater.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.dao.utils.JsonbUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.brokerserver.utils.JsonUtils2; -import lombok.RequiredArgsConstructor; -import org.jooq.JSONB; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * Creates or updates {@link DataOfferRecord} DB Rows. - *

      - * (Or at least prepares them for batch inserts / updates) - */ -@RequiredArgsConstructor -public class DataOfferRecordUpdater { - /** - * Create a new {@link DataOfferRecord}. - * - * @param connectorEndpoint connector endpoint - * @param fetchedDataOffer new db row data - * @return new db row - */ - public DataOfferRecord newDataOffer(String connectorEndpoint, FetchedDataOffer fetchedDataOffer) { - var dataOffer = new DataOfferRecord(); - dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setAssetId(fetchedDataOffer.getAssetId()); - dataOffer.setCreatedAt(OffsetDateTime.now()); - updateDataOffer(dataOffer, fetchedDataOffer, true); - return dataOffer; - } - - - /** - * Update existing {@link DataOfferRecord}. - * - * @param dataOffer existing row - * @param fetchedDataOffer changes to be incorporated - * @param changed whether the data offer should be marked as updated simply because the contract offers changed - * @return whether any fields were updated - */ - public boolean updateDataOffer( - DataOfferRecord dataOffer, - FetchedDataOffer fetchedDataOffer, - boolean changed - ) { - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getAssetTitle, - DataOfferRecord::getAssetTitle, - dataOffer::setAssetTitle - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getDescription, - DataOfferRecord::getDescription, - dataOffer::setDescription - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getCuratorOrganizationName, - DataOfferRecord::getCuratorOrganizationName, - dataOffer::setCuratorOrganizationName - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getDataCategory, - DataOfferRecord::getDataCategory, - dataOffer::setDataCategory - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getDataSubcategory, - DataOfferRecord::getDataSubcategory, - dataOffer::setDataSubcategory - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getDataModel, - DataOfferRecord::getDataModel, - dataOffer::setDataModel - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getTransportMode, - DataOfferRecord::getTransportMode, - dataOffer::setTransportMode - ); - - changed |= updateField( - dataOffer, - fetchedDataOffer, - FetchedDataOffer::getGeoReferenceMethod, - DataOfferRecord::getGeoReferenceMethod, - dataOffer::setGeoReferenceMethod - ); - - changed |= updateKeywords(dataOffer, fetchedDataOffer); - - changed |= updateAssetJsonLd(dataOffer, fetchedDataOffer); - - if (changed) { - dataOffer.setUpdatedAt(OffsetDateTime.now()); - } - - return changed; - } - - private boolean updateField( - DataOfferRecord dataOffer, - FetchedDataOffer fetchedDataOffer, - Function fetchedField, - Function existingField, - Consumer setter - ) { - var fetched = fetchedField.apply(fetchedDataOffer); - if (fetched == null) { - fetched = ""; - } - - var existing = existingField.apply(dataOffer); - if (existing == null) { - existing = ""; - } - - - if (Objects.equals(fetched, existing)) { - return false; - } - - setter.accept(fetched); - return true; - } - - private boolean updateKeywords( - DataOfferRecord dataOffer, - FetchedDataOffer fetchedDataOffer - ) { - List fetched = fetchedDataOffer.getKeywords(); - if (fetched == null) { - fetched = List.of(); - } - - String[] existing = dataOffer.getKeywords(); - if (existing == null) { - existing = new String[0]; - } - - if (Objects.equals(new HashSet<>(fetched), new HashSet<>(Arrays.asList(existing)))) { - return false; - } - - dataOffer.setKeywords(fetched.toArray(new String[0])); - dataOffer.setKeywordsCommaJoined(String.join(",", fetched)); - return true; - } - - private boolean updateAssetJsonLd( - DataOfferRecord dataOffer, - FetchedDataOffer fetchedDataOffer - ) { - String existing = JsonbUtils.getDataOrNull(dataOffer.getAssetJsonLd()); - var fetched = fetchedDataOffer.getAssetJsonLd(); - if (JsonUtils2.isEqualJson(fetched, existing)) { - return false; - } - - dataOffer.setAssetJsonLd(JSONB.jsonb(fetched)); - return true; - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java deleted file mode 100644 index e69e446ee..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.jooq.DSLContext; - -import java.util.Collection; - -@RequiredArgsConstructor -public class DataOfferWriter { - private final DataOfferPatchBuilder dataOfferPatchBuilder; - private final DataOfferPatchApplier dataOfferPatchApplier; - - /** - * Updates a connector's data offers with given {@link FetchedDataOffer}s. - * - * @param dsl dsl - * @param connectorEndpoint connector endpoint - * @param fetchedDataOffers fetched data offers - * @param changes change tracker for log message - */ - @SneakyThrows - public void updateDataOffers(DSLContext dsl, String connectorEndpoint, Collection fetchedDataOffers, ConnectorChangeTracker changes) { - var patch = dataOfferPatchBuilder.buildDataOfferPatch(dsl, connectorEndpoint, fetchedDataOffers); - changes.setNumOffersAdded(patch.getDataOffersToInsert().size()); - changes.setNumOffersUpdated(patch.getDataOffersToUpdate().size()); - changes.setNumOffersDeleted(patch.getDataOffersToDelete().size()); - dataOfferPatchApplier.writeDataOfferPatch(dsl, patch); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java deleted file mode 100644 index 64d850281..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/FetchedCatalogBuilder.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; -import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.catalog.model.DspCatalog; -import de.sovity.edc.utils.catalog.model.DspContractOffer; -import de.sovity.edc.utils.catalog.model.DspDataOffer; -import jakarta.json.JsonObject; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -@RequiredArgsConstructor -public class FetchedCatalogBuilder { - private final AssetMapper assetMapper; - - public FetchedCatalog buildFetchedCatalog(DspCatalog catalog) { - var participantId = catalog.getParticipantId(); - - var fetchedDataOffers = catalog.getDataOffers().stream() - .map(dspDataOffer -> buildFetchedDataOffer(dspDataOffer, participantId)) - .toList(); - - var fetchedCatalog = new FetchedCatalog(); - fetchedCatalog.setParticipantId(participantId); - fetchedCatalog.setDataOffers(fetchedDataOffers); - - return fetchedCatalog; - } - - @NotNull - private FetchedDataOffer buildFetchedDataOffer(DspDataOffer dspDataOffer, String participantId) { - var assetJsonLd = assetMapper.buildAssetJsonLdFromDatasetProperties(dspDataOffer.getAssetPropertiesJsonLd()); - - var fetchedDataOffer = new FetchedDataOffer(); - setAssetMetadata(fetchedDataOffer, assetJsonLd, participantId); - fetchedDataOffer.setContractOffers(buildFetchedContractOffers(dspDataOffer.getContractOffers())); - return fetchedDataOffer; - } - - @NotNull - private List buildFetchedContractOffers(List offers) { - return offers.stream() - .map(this::buildFetchedContractOffer) - .toList(); - } - - @NotNull - private FetchedContractOffer buildFetchedContractOffer(DspContractOffer offer) { - var contractOffer = new FetchedContractOffer(); - contractOffer.setContractOfferId(offer.getContractOfferId()); - contractOffer.setPolicyJson(JsonUtils.toJson(offer.getPolicyJsonLd())); - return contractOffer; - } - - /** - * This method was extract so tests could re-use the logic of assetJsonLd -> fetchedDataOffer -> dataOfferRecord - * - * @param fetchedDataOffer fetchedDataOffer - * @param assetJsonLd assetJsonLd - */ - public void setAssetMetadata(FetchedDataOffer fetchedDataOffer, JsonObject assetJsonLd, String participantId) { - var uiAsset = assetMapper.buildUiAsset(assetJsonLd, "http://if-you-see-this-this-is-a-bug", participantId); - fetchedDataOffer.setAssetId(uiAsset.getAssetId()); - fetchedDataOffer.setAssetJsonLd(JsonUtils.toJson(assetJsonLd)); - - // Most of these fields are extracted so our DB does not need to - // semantically interpret JSON-LD when sorting, searching and filtering - fetchedDataOffer.setAssetTitle(uiAsset.getTitle()); - fetchedDataOffer.setDescription(uiAsset.getDescription()); - fetchedDataOffer.setCuratorOrganizationName(uiAsset.getCreatorOrganizationName()); - - fetchedDataOffer.setDataCategory(uiAsset.getDataCategory()); - fetchedDataOffer.setDataSubcategory(uiAsset.getDataSubcategory()); - fetchedDataOffer.setDataModel(uiAsset.getDataModel()); - fetchedDataOffer.setGeoReferenceMethod(uiAsset.getGeoReferenceMethod()); - fetchedDataOffer.setTransportMode(uiAsset.getTransportMode()); - fetchedDataOffer.setKeywords(uiAsset.getKeywords()); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java deleted file mode 100644 index a7a2b49bd..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/DataOfferPatch.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; - -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -import java.util.ArrayList; -import java.util.List; - -/** - * Contains planned DB Row changes to be applied as batch. - */ -@Getter -@Setter -@FieldDefaults(level = AccessLevel.PRIVATE) -public class DataOfferPatch { - List dataOffersToInsert = new ArrayList<>(); - List dataOffersToUpdate = new ArrayList<>(); - List dataOffersToDelete = new ArrayList<>(); - - List contractOffersToInsert = new ArrayList<>(); - List contractOffersToUpdate = new ArrayList<>(); - List contractOffersToDelete = new ArrayList<>(); - - public void insertDataOffer(DataOfferRecord offer) { - dataOffersToInsert.add(offer); - } - - public void updateDataOffer(DataOfferRecord offer) { - dataOffersToUpdate.add(offer); - } - - public void deleteDataOffer(DataOfferRecord offer) { - dataOffersToDelete.add(offer); - } - - public void insertContractOffer(ContractOfferRecord offer) { - contractOffersToInsert.add(offer); - } - - public void updateContractOffer(ContractOfferRecord offer) { - contractOffersToUpdate.add(offer); - } - - public void deleteContractOffer(ContractOfferRecord offer) { - contractOffersToDelete.add(offer); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java deleted file mode 100644 index a832e20b6..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MdsIdUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.utils; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class MdsIdUtils { - public static String getMdsIdFromParticipantId(String participantId) { - if (participantId == null || !participantId.matches("^MDSL[A-Za-z0-9]+\\.C[A-Za-z0-9]+")) { - return null; - } - - return participantId.split("\\.")[0]; - } - - public static Boolean isValidMdsId(String mdsId) { - return mdsId != null && mdsId.matches("^MDSL[A-Za-z0-9]+"); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java deleted file mode 100644 index 8a5701286..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StreamUtils2.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.utils; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -import java.util.HashSet; -import java.util.function.Function; -import java.util.function.Predicate; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class StreamUtils2 { - - /** - * Returns a predicate that filters out all elements that have the same key as a previous element. - * - * @param keyFn key extractor - * @param item type - * @param key type - * @return predicate to be used in {@link java.util.stream.Stream#filter(Predicate)} - */ - public static Predicate distinctByKey(Function keyFn) { - var keys = new HashSet<>(); - return t -> keys.add(keyFn.apply(t)); - } -} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java b/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java deleted file mode 100644 index ae720063b..000000000 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/UrlUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.utils; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class UrlUtils { - - public static boolean isValidUrl(String url) { - try { - new URL(url).toURI(); - return true; - } catch (MalformedURLException | URISyntaxException e) { - return false; - } - } -} diff --git a/extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index 80d56e9c3..000000000 --- a/extensions/broker-server/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1 +0,0 @@ -de.sovity.edc.ext.brokerserver.BrokerServerExtension diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java deleted file mode 100644 index 5680a20c1..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestAsset.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; -import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; -import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; -import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; -import jakarta.json.JsonObject; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TestAsset { - - public static JsonObject getAssetJsonLd(String assetId) { - return getAssetJsonLd( - UiAssetCreateRequest.builder() - .id(assetId) - .build() - ); - } - - public static JsonObject getAssetJsonLd(UiAssetCreateRequest request) { - var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl("https://example.com") - .build()) - .build(); - var withDataSource = request.toBuilder().dataSource(dataSource).build(); - return buildAssetJsonLdBuilder().createAssetJsonLd(withDataSource, "orgName"); - } - - /** - * Sets assetJsonLd and other extracted fields. - *

      - * This method keeps our tests consistent if we change the extracted fields. - * - * @param dataOfferRecord data offer record to be updated - * @param assetJsonLd asset json ld - * @param participantId required because the organization name will default to the participant id if unset - */ - public static void setDataOfferAssetMetadata(DataOfferRecord dataOfferRecord, JsonObject assetJsonLd, String participantId) { - // We trickily use the real code to update all the extracted values from the asset JSON-LD - var fetchedCatalogBuilder = BrokerServerExtensionContext.instance.fetchedCatalogBuilder(); - var dataOfferRecordUpdater = BrokerServerExtensionContext.instance.dataOfferRecordUpdater(); - - var fetchedDataOffer = new FetchedDataOffer(); - fetchedCatalogBuilder.setAssetMetadata(fetchedDataOffer, assetJsonLd, participantId); - - dataOfferRecord.setAssetId(fetchedDataOffer.getAssetId()); - dataOfferRecordUpdater.updateDataOffer(dataOfferRecord, fetchedDataOffer, false); - } - - public static AssetJsonLdBuilder buildAssetJsonLdBuilder() { - return new AssetJsonLdBuilder( - new DataSourceMapper( - new EdcPropertyUtils(), - new HttpDataSourceMapper(new HttpHeaderMapper()) - ), - buildAssetJsonLdParser(), - new AssetEditRequestMapper() - ); - } - - @NotNull - private static AssetJsonLdParser buildAssetJsonLdParser() { - return new AssetJsonLdParser( - new AssetJsonLdUtils(), - new ShortDescriptionBuilder(), - "https://my-connector"::equals - ); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java deleted file mode 100644 index 3087968d9..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestPolicy.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import de.sovity.edc.ext.brokerserver.client.gen.JSON; -import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteralType; -import de.sovity.edc.utils.JsonUtils; -import org.jooq.JSONB; - -import java.time.OffsetDateTime; -import java.util.List; - -public class TestPolicy { - private static OffsetDateTime today = OffsetDateTime.now(); - - public static UiPolicyConstraint createAfterYesterdayConstraint() { - return UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.GT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(today.minusDays(1).toString()) - .build()) - .build(); - } - - public static de.sovity.edc.client.gen.model.UiPolicyCreateRequest createAfterYesterdayPolicyEdcGen() { - return jsonCast(createAfterYesterdayPolicy(), de.sovity.edc.client.gen.model.UiPolicyCreateRequest.class); - } - - private static R jsonCast(T obj, Class clazz) { - return JSON.deserialize(JSON.serialize(obj), clazz); - } - - public static UiPolicyCreateRequest createAfterYesterdayPolicy() { - return UiPolicyCreateRequest.builder() - .constraints(List.of(createAfterYesterdayConstraint())) - .build(); - } - - public static JSONB createAfterYesterdayPolicyJson() { - var createRequest = TestPolicy.createAfterYesterdayPolicy(); - return getPolicyJsonLd(createRequest); - } - - /** - * This method only works in integration tests, because it depends on the broker server extension context. - */ - public static JSONB getPolicyJsonLd(UiPolicyCreateRequest createRequest) { - var policyMapper = BrokerServerExtensionContext.instance.policyMapper(); - var jsonLd = policyMapper.buildPolicyJsonLd(policyMapper.buildPolicy(createRequest)); - return JSONB.jsonb(JsonUtils.toJson(jsonLd)); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java deleted file mode 100644 index 54a2218d9..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/TestUtils.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver; - -import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; -import de.sovity.edc.ext.brokerserver.client.gen.ApiException; -import de.sovity.edc.ext.brokerserver.db.PostgresFlywayExtension; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import org.assertj.core.api.ThrowableAssert; -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; - -public class TestUtils { - private static final int MANAGEMENT_PORT = getFreePort(); - private static final int PROTOCOL_PORT = getFreePort(); - private static final String MANAGEMENT_PATH = "/api/management"; - private static final String PROTOCOL_PATH = "/api/dsp"; - public static final String MANAGEMENT_API_KEY = "123456"; - public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + MANAGEMENT_PORT + MANAGEMENT_PATH; - public static final String ADMIN_API_KEY = "123456"; - - - public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; - public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH; - public static final String PARTICIPANT_ID = "MDSL1234ZZ.C4321AA"; - public static final String CURATOR_NAME = "My Org"; - - @NotNull - public static Map createConfiguration( - TestDatabase testDatabase, - Map additionalConfigProperties - ) { - Map config = new HashMap<>(); - - config.put("web.http.port", String.valueOf(getFreePort())); - config.put("web.http.path", "/api"); - config.put("web.http.management.port", String.valueOf(MANAGEMENT_PORT)); - config.put("web.http.management.path", MANAGEMENT_PATH); - config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); - config.put("web.http.protocol.path", PROTOCOL_PATH); - config.put("edc.api.auth.key", MANAGEMENT_API_KEY); - config.put("edc.dsp.callback.address", PROTOCOL_ENDPOINT); - config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); - - config.put("edc.participant.id", PARTICIPANT_ID); - config.put("my.edc.participant.id", PARTICIPANT_ID); - config.put("my.edc.title", "My Connector"); - config.put("my.edc.description", "My Connector Description"); - config.put("my.edc.curator.url", "https://connector.my-org"); - config.put("my.edc.curator.name", CURATOR_NAME); - config.put("my.edc.maintainer.url", "https://maintainer-org"); - config.put("my.edc.maintainer.name", "Maintainer Org"); - - config.put(PostgresFlywayExtension.JDBC_URL, testDatabase.getJdbcUrl()); - config.put(PostgresFlywayExtension.JDBC_USER, testDatabase.getJdbcUser()); - config.put(PostgresFlywayExtension.JDBC_PASSWORD, testDatabase.getJdbcPassword()); - config.put(PostgresFlywayExtension.DB_CONNECTION_POOL_SIZE, "20"); - config.put(PostgresFlywayExtension.DB_CONNECTION_TIMEOUT_IN_MS, "3000"); - config.put(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE, "true"); - config.put(PostgresFlywayExtension.FLYWAY_CLEAN, "true"); - config.put(BrokerServerExtension.NUM_THREADS, "0"); - config.put(BrokerServerExtension.ADMIN_API_KEY, ADMIN_API_KEY); - config.putAll(getCoreEdcJdbcConfig(testDatabase)); - config.putAll(additionalConfigProperties); - return config; - } - - private static Map getCoreEdcJdbcConfig(TestDatabase testDatabase) { - Map config = new HashMap<>(); - List.of("asset", - "contractdefinition", - "contractnegotiation", - "policy", - "transferprocess", - "dataplaneinstance" - ).forEach(it -> { - config.put("edc.datasource.%s.name".formatted(it), it); - config.put("edc.datasource.%s.url".formatted(it), testDatabase.getJdbcUrl()); - config.put("edc.datasource.%s.user".formatted(it), testDatabase.getJdbcUser()); - config.put("edc.datasource.%s.password".formatted(it), testDatabase.getJdbcPassword()); - }); - return config; - } - - public static BrokerServerClient brokerServerClient() { - return BrokerServerClient.builder() - .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) - .managementApiKey(TestUtils.MANAGEMENT_API_KEY) - .build(); - } - - - public static void assertIs401(ThrowableAssert.ThrowingCallable callable) { - assertThatThrownBy(callable) - .isInstanceOf(ApiException.class) - .satisfies(ex -> { - var apiException = (ApiException) ex; - assertThat(apiException.getCode()).isEqualTo(401); - }); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java deleted file mode 100644 index 13fe324ec..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/dao/utils/LikeUtilsTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.dao.utils; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class LikeUtilsTest { - @Test - void escape() { - assertThat(LikeUtils.escape("a\\b_c%d")).isEqualTo("a\\\\b\\_c\\%d"); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java deleted file mode 100644 index 059f81474..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/FlywayTestUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.eclipse.edc.spi.monitor.ConsoleMonitor; -import org.eclipse.edc.spi.system.configuration.Config; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class FlywayTestUtils { - - public static void migrate(TestDatabase testDatabase) { - var monitor = new ConsoleMonitor(); - var config = mock(Config.class); - when(config.getBoolean(eq(PostgresFlywayExtension.FLYWAY_CLEAN_ENABLE), any())).thenReturn(true); - when(config.getBoolean(eq(PostgresFlywayExtension.FLYWAY_CLEAN), any())).thenReturn(true); - - var flywayFactory = new FlywayFactory(config); - var dataSource = testDatabase.getDataSource(); - var flyway = flywayFactory.setupFlyway(dataSource); - var flywayMigrator = new FlywayMigrator(flyway, config, monitor); - flywayMigrator.migrateAndRepair(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java deleted file mode 100644 index e593df41c..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabase.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import de.sovity.edc.ext.brokerserver.db.utils.JdbcCredentials; -import org.jooq.DSLContext; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; - -import javax.sql.DataSource; -import java.util.function.Consumer; - -public interface TestDatabase extends BeforeAllCallback, AfterAllCallback { - String getJdbcUrl(); - - String getJdbcUser(); - - String getJdbcPassword(); - - /** - * New {@link DslContextFactory} from the test database's credentials - * - * @return {@link DslContextFactory} - */ - default DslContextFactory getDslContextFactory() { - var dataSource = getDataSource(); - return new DslContextFactory(dataSource); - } - - /** - * Returns a {@link DataSource} to the test database - * - * @return {@link DataSource} - */ - default DataSource getDataSource() { - var jdbcCredentials = new JdbcCredentials(getJdbcUrl(), getJdbcUser(), getJdbcPassword()); - return DataSourceFactory.newDataSource(jdbcCredentials, 20, 30_000); - } - - /** - * Runs given code within a test transaction. - *
      - * Globally hijacks all {@link DslContextFactory}s to use this test transaction. - * - * @param code code to run within the test transaction - */ - default void testTransaction(Consumer code) { - try { - getDslContextFactory().transaction(dsl -> DslContextFactoryHijacker.withParentDslContext(dsl, () -> { - code.accept(dsl); - throw new TestDatabaseCancelTransactionException(); - })); - } catch (TestDatabaseCancelTransactionException e) { - // Ignore - } - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java deleted file mode 100644 index cc272fcb8..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TestDatabaseFactory { - - /** - * Returns a JUnit 5 Extension that launches a testcontainer. - * - * @return {@link TestDatabase} - */ - public static TestDatabase getTestDatabase() { - return new TestDatabaseViaTestcontainers(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java deleted file mode 100644 index ad2a5c5f9..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaEnv.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import org.apache.commons.lang3.Validate; -import org.junit.jupiter.api.extension.ExtensionContext; - -public class TestDatabaseViaEnv implements TestDatabase { - public static final String SKIP_TESTCONTAINERS = "SKIP_TESTCONTAINERS"; - public static final String TEST_POSTGRES_JDBC_URL = "TEST_POSTGRES_JDBC_URL"; - public static final String TEST_POSTGRES_JDBC_USER = "TEST_POSTGRES_JDBC_USER"; - public static final String TEST_POSTGRES_JDBC_PASSWORD = "TEST_POSTGRES_JDBC_PASSWORD"; - - @Override - public void afterAll(ExtensionContext context) throws Exception { - - } - - @Override - public void beforeAll(ExtensionContext context) throws Exception { - - } - - public String getJdbcUrl() { - return getRequiredEnv(TEST_POSTGRES_JDBC_URL); - } - - public String getJdbcUser() { - return getRequiredEnv(TEST_POSTGRES_JDBC_USER); - } - - public String getJdbcPassword() { - return getRequiredEnv(TEST_POSTGRES_JDBC_PASSWORD); - } - - private static String getRequiredEnv(String name) { - String value = System.getenv(name); - Validate.notBlank(value, "Need env var %s since %s is true", name, SKIP_TESTCONTAINERS); - return value; - } - - public static boolean isSkipTestcontainers() { - return "true".equals(System.getenv(SKIP_TESTCONTAINERS)); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java deleted file mode 100644 index fd3d1caf0..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseViaTestcontainers.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.db; - -import de.sovity.edc.utils.versions.GradleVersions; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.PostgreSQLContainer; - -public class TestDatabaseViaTestcontainers implements TestDatabase { - private PostgreSQLContainer container = new PostgreSQLContainer<>(GradleVersions.POSTGRES_IMAGE_TAG) - .withUsername("edc") - .withPassword("edc"); - - @Override - public void afterAll(ExtensionContext context) throws Exception { - container.stop(); - } - - @Override - public void beforeAll(ExtensionContext context) throws Exception { - container.start(); - } - - public String getJdbcUrl() { - return container.getJdbcUrl(); - } - - public String getJdbcUser() { - return container.getUsername(); - } - - public String getJdbcPassword() { - return container.getPassword(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java deleted file mode 100644 index 058882217..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AddConnectorsApiTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.TestUtils; -import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; -import de.sovity.edc.ext.brokerserver.client.gen.model.AddedConnector; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorCreationRequest; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class AddConnectorsApiTest { - BrokerServerClient client; - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - client = brokerServerClient(); - } - - @Test - void testAddConnectors() { - TEST_DATABASE.testTransaction(dsl -> { - client.brokerServerApi().addConnectors(ADMIN_API_KEY, List.of()); - - client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( - null, - "", - " ", - "\t", - "http://a", - "http://b" - )); - - assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactlyInAnyOrder("http://a", "http://b"); - - client.brokerServerApi().addConnectors(ADMIN_API_KEY, Arrays.asList( - "http://b", - " http://b\r\n", - "http://c" - )); - - assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); - }); - } - - @Test - void testAddConnectorsWithMdsIds() { - TEST_DATABASE.testTransaction(dsl -> { - client.brokerServerApi().addConnectorsWithMdsIds(ADMIN_API_KEY, new ConnectorCreationRequest(List.of())); - - client.brokerServerApi().addConnectorsWithMdsIds(ADMIN_API_KEY, new ConnectorCreationRequest(Arrays.asList( - new AddedConnector( - null, - "MDSL1234" - ), - new AddedConnector( - "", - "MDSL1234" - ), - new AddedConnector( - " ", - "MDSL1234" - ), - new AddedConnector( - "\t", - "MDSL1234" - ), - new AddedConnector( - "http://a", - "MDSL1234" - ), - new AddedConnector( - " http://b\r\n", - "MDSL1234" - ), - new AddedConnector( - "http://c", - null - ), - new AddedConnector( - "http://d", - "" - ), - new AddedConnector( - "http://e", - " " - ), - new AddedConnector( - "http://f", - "\t" - ) - ))); - - assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactlyInAnyOrder("http://a", "http://b"); - - client.brokerServerApi().addConnectorsWithMdsIds(ADMIN_API_KEY, new ConnectorCreationRequest(Arrays.asList( - new AddedConnector( - "http://b", - "MDSL1234" - ), - new AddedConnector( - " http://b\r\n", - "MDSL1234" - ), - new AddedConnector( - "http://c", - "MDSL1234" - ) - ))); - - assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactlyInAnyOrder("http://a", "http://b", "http://c"); - }); - } - - @Test - void testAddWrongApiKey() { - TEST_DATABASE.testTransaction(dsl -> TestUtils.assertIs401(() -> - client.brokerServerApi().addConnectors("wrong-api-key", List.of()))); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java deleted file mode 100644 index f8c230ab6..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalConnectorMetadataApiTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.TestPolicy; -import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalConnectorInfo; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class AuthorityPortalConnectorMetadataApiTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - } - - @Test - void testConnectorMetadataByEndpoints() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var now = OffsetDateTime.now().withNano(0); - - createConnector(dsl, now, 1); - createDataOffer(dsl, now, 1, 1); - createDataOffer(dsl, now, 1, 2); - - createConnector(dsl, now, 2); - createDataOffer(dsl, now, 2, 1); - - createConnector(dsl, now, 3); - createDataOffer(dsl, now, 3, 1); - - createConnector(dsl, now, 4); - - // act - var actual = brokerServerClient().brokerServerApi().getConnectorMetadata( - ADMIN_API_KEY, - Arrays.asList( - getEndpoint(1), - getEndpoint(1), // having this twice should not crash the query - getEndpoint(2), - getEndpoint(4), - getEndpoint(5) // having this not existing should not crash the query - )); - - // assert - var first = forConnector(actual, 1); - assertThat(first.getParticipantId()).isEqualTo("my-connector"); - assertThat(first.getDataOfferCount()).isEqualTo(2); - assertThat(first.getOnlineStatus()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE); - assertThat(first.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); - var second = forConnector(actual, 2); - assertThat(second.getDataOfferCount()).isEqualTo(1); - assertThat(second.getOnlineStatus()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE); - assertThat(second.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); - var fourth = forConnector(actual, 4); - assertThat(fourth.getDataOfferCount()).isEqualTo(0); - assertThat(fourth.getOnlineStatus()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE); - assertThat(fourth.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); - }); - } - - private AuthorityPortalConnectorInfo forConnector(List actual, int iConnector) { - return actual.stream() - .filter(connectorMetadata -> - getEndpoint(iConnector).equals(connectorMetadata.getConnectorEndpoint()) - ) - .findFirst() - .orElseThrow(); - } - - private void createConnector(DSLContext dsl, OffsetDateTime now, int iConnector) { - var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setParticipantId("my-connector"); - connector.setEndpoint(getEndpoint(iConnector)); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(now.minusDays(1)); - connector.setLastRefreshAttemptAt(now); - connector.setLastSuccessfulRefreshAt(now); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - connector.insert(); - } - - private String getEndpoint(int iConnector) { - return "https://connector-%d".formatted(iConnector); - } - - private void createDataOffer(DSLContext dsl, OffsetDateTime now, int iConnector, int iDataOffer) { - var connectorEndpoint = getEndpoint(iConnector); - var assetJsonLd = getAssetJsonLd("my-asset-%d".formatted(iDataOffer)); - - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); - dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setCreatedAt(now.minusDays(5)); - dataOffer.setUpdatedAt(now); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); - contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(dataOffer.getAssetId()); - contractOffer.setCreatedAt(now.minusDays(5)); - contractOffer.setUpdatedAt(now); - contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); - contractOffer.insert(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java deleted file mode 100644 index 9cd29b55f..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalDataOfferApiTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.TestPolicy; -import de.sovity.edc.ext.brokerserver.client.gen.ApiException; -import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalConnectorDataOfferInfo; -import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalConnectorInfo; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThrows; - -@ApiTest -@ExtendWith(EdcExtension.class) -class AuthorityPortalDataOfferApiTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - } - - @Test - void testAuthenticationOfEndpoints() { - TEST_DATABASE.testTransaction(dsl -> { - var code = assertThrows(ApiException.class, () -> brokerServerClient().brokerServerApi().getConnectorDataOffers( - ADMIN_API_KEY + "invalid", - List.of())).getCode(); - assertThat(code).isEqualTo(401); - }); - } - - @Test - void testConnectorMetadataByEndpoints() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var now = OffsetDateTime.now().withNano(0); - - createConnector(dsl, now, 1); - createDataOffer(dsl, now, 1, 1); - createDataOffer(dsl, now, 1, 2); - - createConnector(dsl, now, 2); - createDataOffer(dsl, now, 2, 1); - - createConnector(dsl, now, 4); - - // act - var actual = brokerServerClient().brokerServerApi().getConnectorDataOffers( - ADMIN_API_KEY, - Arrays.asList( - getEndpoint(1), - getEndpoint(2), - getEndpoint(4) - )); - - // assert - // connector 1 with two data offer - var connector1 = forConnector(actual, 1); - assertThat(connector1.getConnectorEndpoint()).isEqualTo(getEndpoint(1)); - assertThat(connector1.getParticipantId()).isEqualTo("my-connector"); - assertThat(connector1.getOnlineStatus().getValue()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE.getValue()); - assertThat(connector1.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); - assertThat(connector1.getDataOffers().size()).isEqualTo(2); - var connector1asset1 = connector1.getDataOffers().stream().filter(dataOffer -> dataOffer.getDataOfferId().equals(getAssetId(1))).findFirst().orElseThrow(); - assertThat(connector1asset1.getDataOfferId()).isEqualTo("my-asset-1"); - assertThat(connector1asset1.getDataOfferName()).isEqualTo("my-asset-1"); - var connector1asset2 = connector1.getDataOffers().stream().filter(dataOffer -> dataOffer.getDataOfferId().equals(getAssetId(2))).findFirst().orElseThrow(); - assertThat(connector1asset2.getDataOfferId()).isEqualTo("my-asset-2"); - assertThat(connector1asset2.getDataOfferName()).isEqualTo("my-asset-2"); - - // connector 2 with one data offer - var connector2 = forConnector(actual, 2); - assertThat(connector2.getConnectorEndpoint()).isEqualTo(getEndpoint(2)); - assertThat(connector2.getParticipantId()).isEqualTo("my-connector"); - assertThat(connector2.getOnlineStatus().getValue()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE.getValue()); - assertThat(connector2.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); - assertThat(connector2.getDataOffers().size()).isEqualTo(1); - var connector2asset1 = connector2.getDataOffers().stream().filter(dataOffer -> dataOffer.getDataOfferId().equals(getAssetId(1))).findFirst().orElseThrow(); - assertThat(connector2asset1.getDataOfferId()).isEqualTo("my-asset-1"); - assertThat(connector2asset1.getDataOfferName()).isEqualTo("my-asset-1"); - - // connector 4 without data offers - var connector4 = forConnector(actual, 4); - assertThat(connector4.getConnectorEndpoint()).isEqualTo(getEndpoint(4)); - assertThat(connector4.getParticipantId()).isEqualTo("my-connector"); - assertThat(connector4.getOnlineStatus().getValue()).isEqualTo(AuthorityPortalConnectorInfo.OnlineStatusEnum.ONLINE.getValue()); - assertThat(connector4.getOfflineSinceOrLastUpdatedAt()).isEqualTo(now); - assertThat(connector4.getDataOffers().size()).isEqualTo(0); - }); - } - - private AuthorityPortalConnectorDataOfferInfo forConnector(List actual, int iConnector) { - return actual.stream() - .filter(connectorMetadata -> - getEndpoint(iConnector).equals(connectorMetadata.getConnectorEndpoint()) - ) - .findFirst() - .orElseThrow(); - } - - private void createConnector(DSLContext dsl, OffsetDateTime now, int iConnector) { - var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setParticipantId("my-connector"); - connector.setEndpoint(getEndpoint(iConnector)); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(now.minusDays(1)); - connector.setLastRefreshAttemptAt(now); - connector.setLastSuccessfulRefreshAt(now); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - connector.insert(); - } - - private String getEndpoint(int iConnector) { - return "https://connector-%d".formatted(iConnector); - } - - private String getAssetId(int iDataOffer) { - return "my-asset-%d".formatted(iDataOffer); - } - - private void createDataOffer(DSLContext dsl, OffsetDateTime now, int iConnector, int iDataOffer) { - var connectorEndpoint = getEndpoint(iConnector); - var assetJsonLd = getAssetJsonLd(getAssetId(iDataOffer)); - - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); - dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setCreatedAt(now.minusDays(5)); - dataOffer.setUpdatedAt(now); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); - contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(dataOffer.getAssetId()); - contractOffer.setCreatedAt(now.minusDays(5)); - contractOffer.setUpdatedAt(now); - contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); - contractOffer.insert(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java deleted file mode 100644 index fa907770b..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/AuthorityPortalOrganizationMetadataApiTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalOrganizationMetadata; -import de.sovity.edc.ext.brokerserver.client.gen.model.AuthorityPortalOrganizationMetadataRequest; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.OrganizationMetadataRecord; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class AuthorityPortalOrganizationMetadataApiTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - } - - @Test - void testSetOrganizationMetadata() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - createOrgMetadataInDb(dsl, "MDSL1111AA", "Test Org A"); - createOrgMetadataInDb(dsl, "MDSL2222BB", "Test Org B"); - createOrgMetadataInDb(dsl, "MDSL3333CC", "Test Org C"); - - // act - var orgMetadataRequest = new AuthorityPortalOrganizationMetadataRequest(); - orgMetadataRequest.setOrganizations(List.of( - buildOrgMetadataRequestEntry("MDSL2222BB", "Test Org B"), - buildOrgMetadataRequestEntry("MDSL3333CC", "Test Org C new"), - buildOrgMetadataRequestEntry("MDSL4444DD", "Test Org D") - )); - - brokerServerClient().brokerServerApi().setOrganizationMetadata( - ADMIN_API_KEY, - orgMetadataRequest - ); - - // assert - var orgMetadata = getOrgMetadataFromDb(dsl); - assertThat(orgMetadata).hasSize(3); - assertThat(orgMetadata).extracting(OrganizationMetadataRecord::getName).containsExactlyInAnyOrder("Test Org B", "Test Org C new", "Test Org D"); - }); - } - - @Test - void testSetEmptyOrganizationMetadata() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - createOrgMetadataInDb(dsl, "MDSL1111AA", "Test Org A"); - - // act - var orgMetadataRequest = new AuthorityPortalOrganizationMetadataRequest(); - orgMetadataRequest.setOrganizations(List.of()); - - brokerServerClient().brokerServerApi().setOrganizationMetadata( - ADMIN_API_KEY, - orgMetadataRequest - ); - - // assert - var orgMetadata = getOrgMetadataFromDb(dsl); - assertThat(orgMetadata).isEmpty(); - }); - } - - private void createOrgMetadataInDb(DSLContext dsl, String mdsId, String name) { - var organizationMetadata = dsl.newRecord(Tables.ORGANIZATION_METADATA); - organizationMetadata.setMdsId(mdsId); - organizationMetadata.setName(name); - organizationMetadata.insert(); - } - - private List getOrgMetadataFromDb(DSLContext dsl) { - return dsl.selectFrom(Tables.ORGANIZATION_METADATA).fetch(); - } - - private AuthorityPortalOrganizationMetadata buildOrgMetadataRequestEntry(String mdsId, String name) { - var orgMetadata = new AuthorityPortalOrganizationMetadata(); - orgMetadata.setMdsId(mdsId); - orgMetadata.setName(name); - return orgMetadata; - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java deleted file mode 100644 index fb508c39d..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/CatalogApiTest.java +++ /dev/null @@ -1,708 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.ext.brokerserver.BrokerServerExtension; -import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogDataOffer; -import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; -import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageResult; -import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterAttribute; -import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterItem; -import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValue; -import de.sovity.edc.ext.brokerserver.client.gen.model.CnfFilterValueAttribute; -import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageResult; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import jakarta.json.JsonObject; -import lombok.SneakyThrows; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.policy.model.Policy; -import org.eclipse.edc.policy.model.PolicyType; -import org.jooq.DSLContext; -import org.jooq.JSONB; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static java.util.stream.IntStream.range; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class CatalogApiTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - BrokerServerExtension.CATALOG_PAGE_PAGE_SIZE, "10", - BrokerServerExtension.DEFAULT_CONNECTOR_DATASPACE, "MDS", - BrokerServerExtension.KNOWN_DATASPACE_CONNECTORS, "Example1=https://my-connector2/api/dsp,Example2=https://my-connector3/api/dsp" - ))); - } - - @Test - void testDataSpace_two_dataspaces_filter_for_one() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset") - .title("My Asset") - .build() - ); - - // Dataspace: MDS - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); - - // Dataspace: Example1 - createConnector(dsl, today, "https://my-connector2/api/dsp"); - createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd); - - var query = new CatalogPageQuery(); - query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataSpace", List.of("Example1")) - ))); - - var result = brokerServerClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).hasSize(1); - - var dataOfferResult = result.getDataOffers().get(0); - assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("https://my-connector2/api/dsp"); - }); - } - - @Test - void testConnectorEndpointFilter_two_connectors_filter_for_one() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset") - .title("My Asset") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); - - createConnector(dsl, today, "https://my-connector2/api/dsp"); - createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd); - - var query = new CatalogPageQuery(); - query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("connectorEndpoint", List.of("https://my-connector/api/dsp")) - ))); - - var result = brokerServerClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly("https://my-connector/api/dsp"); - }); - } - - @Test - void test_available_filter_values_to_filter_by() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - createConnector(dsl, today, "https://my-connector/api/dsp"); // Dataspace: MDS - createConnector(dsl, today, "https://my-connector2/api/dsp"); // Dataspace: Example1 - createConnector(dsl, today, "https://my-connector3/api/dsp"); // Dataspace: Example2 - - var assetJsonLd1 = getAssetJsonLd("my-asset-1"); - var assetJsonLd2 = getAssetJsonLd("my-asset-2"); - var assetJsonLd3 = getAssetJsonLd("my-asset-3"); - - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); // Dataspace: MDS - createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd1); // Dataspace: Example1 - createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd2); // Dataspace: Example1 - createDataOffer(dsl, today, "https://my-connector3/api/dsp", assetJsonLd3); // Dataspace: Example2 - - // get all available filter values - var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); - - // assert that the filter values are correct - var dataSpace = getAvailableFilter(result, "dataSpace"); - assertThat(dataSpace.getValues()).containsExactly( - new CnfFilterItem("Example1", "Example1"), - new CnfFilterItem("Example2", "Example2"), - new CnfFilterItem("MDS", "MDS") - ); - }); - } - - @Test - void testDataOfferDetails() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset") - .title("My Asset") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); - - var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); - assertThat(result.getDataOffers()).hasSize(1); - - var dataOfferResult = result.getDataOffers().get(0); - assertThat(dataOfferResult.getConnectorEndpoint()).isEqualTo("https://my-connector/api/dsp"); - assertThat(dataOfferResult.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); - assertThat(dataOfferResult.getConnectorOnlineStatus()).isEqualTo(CatalogDataOffer.ConnectorOnlineStatusEnum.ONLINE); - assertThat(dataOfferResult.getAssetId()).isEqualTo("my-asset"); - assertThat(dataOfferResult.getAsset().getAssetId()).isEqualTo("my-asset"); - assertThat(dataOfferResult.getAsset().getTitle()).isEqualTo("My Asset"); - assertThat(dataOfferResult.getCreatedAt()).isEqualTo(today.minusDays(5)); - }); - } - - /** - * Tests against an issue where empty available filter values resulted in NULLs - */ - @Test - void testEmptyConnector() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - createConnector(dsl, today, "https://my-connector/api/dsp"); - - // act - var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); - - // assert - assertThat(result.getDataOffers()).isEmpty(); - assertThat(result.getAvailableFilters().getFields()).isNotEmpty(); - assertThat(result.getAvailableSortings()).isNotEmpty(); - - // the most important thing is that the above code ran through as it crashed before - }); - } - - @Test - void testAvailableFilters_noFilter() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd1 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-1") - .dataCategory("my-category-1") - .transportMode("MY-TRANSPORT-MODE-1") - .dataSubcategory("MY-SUBCATEGORY-2") - .dataModel("my-data-model") - .geoReferenceMethod("my-geo-ref") - .build() - ); - - var assetJsonLd2 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-2") - .dataCategory("my-category-1") - .transportMode("my-transport-mode-2") - .dataSubcategory("MY-SUBCATEGORY-2") - .build() - ); - - var assetJsonLd3 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-3") - .dataCategory("my-category-1") - .transportMode("MY-TRANSPORT-MODE-1") - .dataSubcategory("my-subcategory-1") - .build() - ); - - var assetJsonLd4 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-4") - .dataCategory("my-category-1") - .transportMode("") - .build() - ); - - createOrganizationMetadata(dsl, "MDSL123456AA", "Test Org"); - createConnector(dsl, today, "https://my-connector/api/dsp", "MDSL123456AA"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd2); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd3); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd4); - - var result = brokerServerClient().brokerServerApi().catalogPage(new CatalogPageQuery()); - - assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getId) - .containsExactly( - "dataSpace", - "dataCategory", - "dataSubcategory", - "dataModel", - "transportMode", - "geoReferenceMethod", - "curatorOrganizationName", - "curatorMdsId", - "connectorEndpoint" - ); - - assertThat(result.getAvailableFilters().getFields()) - .extracting(CnfFilterAttribute::getTitle) - .containsExactly( - "Data Space", - "Data Category", - "Data Subcategory", - "Data Model", - "Transport Mode", - "Geo Reference Method", - "Organization Name", - "MDS ID", - "Connector" - ); - - var dataSpace = getAvailableFilter(result, "dataSpace"); - assertThat(dataSpace.getValues()).extracting(CnfFilterItem::getId).containsExactly("MDS"); - assertThat(dataSpace.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MDS"); - - var dataCategory = getAvailableFilter(result, "dataCategory"); - assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-category-1"); - assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-category-1"); - - var dataSubcategory = getAvailableFilter(result, "dataSubcategory"); - assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); - assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-subcategory-1", "MY-SUBCATEGORY-2", ""); - - var dataModel = getAvailableFilter(result, "dataModel"); - assertThat(dataModel.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-data-model", ""); - assertThat(dataModel.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-data-model", ""); - - var transportMode = getAvailableFilter(result, "transportMode"); - assertThat(transportMode.getValues()).extracting(CnfFilterItem::getId).containsExactly("MY-TRANSPORT-MODE-1", "my-transport-mode-2", ""); - assertThat(transportMode.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MY-TRANSPORT-MODE-1", "my-transport-mode-2", ""); - - var geoReferenceMethod = getAvailableFilter(result, "geoReferenceMethod"); - assertThat(geoReferenceMethod.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-geo-ref", ""); - assertThat(geoReferenceMethod.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-geo-ref", ""); - - var curatorOrganizationName = getAvailableFilter(result, "curatorOrganizationName"); - assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getId).containsExactly("Test Org"); - assertThat(curatorOrganizationName.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("Test Org"); - - var curatorMdsId = getAvailableFilter(result, "curatorMdsId"); - assertThat(curatorMdsId.getValues()).extracting(CnfFilterItem::getId).containsExactly("MDSL123456AA"); - assertThat(curatorMdsId.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("MDSL123456AA"); - - var connectorEndpoint = getAvailableFilter(result, "connectorEndpoint"); - assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getId).containsExactly("https://my-connector/api/dsp"); - assertThat(connectorEndpoint.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("https://my-connector/api/dsp"); - }); - } - - - /** - * Regression Test against bug where asset names with capital letters were not hit by search. - *
      - * It was caused by search terms getting lower cased while the LIKE operation being case-sensitive. - */ - @Test - void testSearchCaseInsensitive() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("123") - .title("Hello") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); - - - // act - var query = new CatalogPageQuery(); - query.setSearchQuery("Hello"); - var result = brokerServerClient().brokerServerApi().catalogPage(query); - - // assert - assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly("123"); - }); - } - - private CnfFilterAttribute getAvailableFilter(CatalogPageResult result, String filterId) { - return result.getAvailableFilters().getFields().stream() - .filter(it -> it.getId().equals(filterId)).findFirst() - .orElseThrow(() -> new IllegalStateException("Filter not found")); - } - - @Test - void testAvailableFilters_withFilter() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd1 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-1") - .dataCategory("my-category") - .dataSubcategory("my-subcategory") - .build() - ); - - var assetJsonLd2 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-2") - .dataSubcategory("my-other-subcategory") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd2); - - var query = new CatalogPageQuery(); - query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("dataCategory", List.of("")) - ))); - - var result = brokerServerClient().brokerServerApi().catalogPage(query); - - var dataCategory = getAvailableFilter(result, "dataCategory"); - assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-category", ""); - assertThat(dataCategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-category", ""); - - var dataSubcategory = getAvailableFilter(result, "dataSubcategory"); - assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getId).containsExactly("my-other-subcategory"); - assertThat(dataSubcategory.getValues()).extracting(CnfFilterItem::getTitle).containsExactly("my-other-subcategory"); - }); - } - - @Test - void testPagination_firstPage() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("my-asset-%d".formatted(i)))); - range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("some-other-asset-%d".formatted(i)))); - - var query = new CatalogPageQuery(); - query.setSearchQuery("my-asset"); - query.setSorting(CatalogPageQuery.SortingEnum.TITLE); - - var result = brokerServerClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(range(0, 10).mapToObj("my-asset-%d"::formatted).toList()); - - var actual = result.getPaginationMetadata(); - assertThat(actual.getPageOneBased()).isEqualTo(1); - assertThat(actual.getPageSize()).isEqualTo(10); - assertThat(actual.getNumVisible()).isEqualTo(10); - assertThat(actual.getNumTotal()).isEqualTo(15); - }); - } - - @Test - void testPagination_secondPage() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("my-asset-%d".formatted(i)))); - range(0, 15).forEach(i -> createDataOffer(dsl, today, "https://my-connector/api/dsp", getAssetJsonLd("some-other-asset-%d".formatted(i)))); - - - var query = new CatalogPageQuery(); - query.setSearchQuery("my-asset"); - query.setPageOneBased(2); - query.setSorting(CatalogPageQuery.SortingEnum.TITLE); - - var result = brokerServerClient().brokerServerApi().catalogPage(query); - - assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId) - .isEqualTo(range(10, 15).mapToObj("my-asset-%d"::formatted).toList()); - - var actual = result.getPaginationMetadata(); - assertThat(actual.getPageOneBased()).isEqualTo(2); - assertThat(actual.getPageSize()).isEqualTo(10); - assertThat(actual.getNumVisible()).isEqualTo(5); - assertThat(actual.getNumTotal()).isEqualTo(15); - }); - } - - @Test - void testSortingByPopularity() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var endpoint = "https://my-connector/api/dsp"; - createConnector(dsl, today, endpoint); - createDataOffer(dsl, today, endpoint, getAssetJsonLd("asset-1")); - createDataOffer(dsl, today, endpoint, getAssetJsonLd("asset-2")); - createDataOffer(dsl, today, endpoint, getAssetJsonLd("asset-3")); - - range(0, 3).forEach(i -> dataOfferDetails(endpoint, "asset-1")); - range(0, 5).forEach(i -> dataOfferDetails(endpoint, "asset-2")); - - - var query = new CatalogPageQuery(); - query.setSorting(CatalogPageQuery.SortingEnum.VIEW_COUNT); - - var result = brokerServerClient().brokerServerApi().catalogPage(query); - assertThat(result.getDataOffers()).extracting(CatalogDataOffer::getAssetId).containsExactly( - "asset-2", - "asset-1", - "asset-3" - ); - }); - } - - @Test - void testFilterByOrgName() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var endpoint1 = "https://my-connector-1/api/dsp"; - createConnector(dsl, today, endpoint1, "MDSL1111AA"); - createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); - - var endpoint2 = "https://my-connector-2/api/dsp"; - createConnector(dsl, today, endpoint2, "MDSL2222BB"); - createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); - - createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); - - // act - var query = new CatalogPageQuery(); - query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("curatorOrganizationName", List.of("Test Org")) - ))); - - var actual = brokerServerClient().brokerServerApi().catalogPage(query); - - // assert - assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint1); - }); - } - - @Test - void testSearchForOrgName() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var endpoint1 = "https://my-connector-1/api/dsp"; - createConnector(dsl, today, endpoint1, "MDSL1111AA"); - createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); - - var endpoint2 = "https://my-connector-2/api/dsp"; - createConnector(dsl, today, endpoint2, "MDSL2222BB"); - createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); - - createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); - - // act - var query = new CatalogPageQuery(); - query.setSearchQuery("tEsT"); - - var actual = brokerServerClient().brokerServerApi().catalogPage(query); - - // assert - assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint1); - }); - } - - @Test - void testFilterByMdsId() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var endpoint1 = "https://my-connector-1/api/dsp"; - createConnector(dsl, today, endpoint1, "MDSL1111AA"); - createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); - - var endpoint2 = "https://my-connector-2/api/dsp"; - createConnector(dsl, today, endpoint2, "MDSL2222BB"); - createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); - - // act - var query = new CatalogPageQuery(); - query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("curatorMdsId", List.of("MDSL1111AA")) - ))); - - var actual = brokerServerClient().brokerServerApi().catalogPage(query); - - // assert - assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint1); - }); - } - - @Test - void testFilterByUnknown() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var endpoint1 = "https://my-connector-1/api/dsp"; - createConnector(dsl, today, endpoint1, "MDSL1111AA"); - createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); - - var endpoint2 = "https://my-connector-2/api/dsp"; - createConnector(dsl, today, endpoint2, "MDSL2222BB"); - createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); - - createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); - - // act - var query = new CatalogPageQuery(); - query.setFilter(new CnfFilterValue(List.of( - new CnfFilterValueAttribute("curatorOrganizationName", List.of("Unknown")) - ))); - - var actual = brokerServerClient().brokerServerApi().catalogPage(query); - - // assert - assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint2); - }); - } - - @Test - void testSearchForUnknown() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var today = OffsetDateTime.now().withNano(0); - - var endpoint1 = "https://my-connector-1/api/dsp"; - createConnector(dsl, today, endpoint1, "MDSL1111AA"); - createDataOffer(dsl, today, endpoint1, getAssetJsonLd("asset-1")); - - var endpoint2 = "https://my-connector-2/api/dsp"; - createConnector(dsl, today, endpoint2, "MDSL2222BB"); - createDataOffer(dsl, today, endpoint2, getAssetJsonLd("asset-2")); - - createOrganizationMetadata(dsl, "MDSL1111AA", "Test Org"); - - // act - var query = new CatalogPageQuery(); - query.setSearchQuery("uNkN"); - - var actual = brokerServerClient().brokerServerApi().catalogPage(query); - - // assert - assertThat(actual.getDataOffers()).extracting(CatalogDataOffer::getConnectorEndpoint).containsExactly(endpoint2); - }); - } - - private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); - dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setCreatedAt(today.minusDays(5)); - dataOffer.setUpdatedAt(today); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); - contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(dataOffer.getAssetId()); - contractOffer.setCreatedAt(today.minusDays(5)); - contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(JSONB.jsonb(policyToJson(dummyPolicy()))); - contractOffer.insert(); - } - - private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { - createConnector(dsl, today, connectorEndpoint, null); - } - - private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, String mdsId) { - var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setParticipantId("my-connector"); - connector.setMdsId(mdsId); - connector.setEndpoint(connectorEndpoint); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(today.minusDays(1)); - connector.setLastRefreshAttemptAt(today); - connector.setLastSuccessfulRefreshAt(today); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - connector.insert(); - } - - private void createOrganizationMetadata(DSLContext dsl, String mdsId, String name) { - var organizationMetadata = dsl.newRecord(Tables.ORGANIZATION_METADATA); - organizationMetadata.setMdsId(mdsId); - organizationMetadata.setName(name); - organizationMetadata.insert(); - } - - private Policy dummyPolicy() { - return Policy.Builder.newInstance() - .type(PolicyType.SET) - .build(); - } - - private DataOfferDetailPageResult dataOfferDetails(String endpoint, String assetId) { - var query = DataOfferDetailPageQuery.builder() - .connectorEndpoint(endpoint) - .assetId(assetId) - .build(); - return brokerServerClient().brokerServerApi().dataOfferDetailPage(query); - } - - private String policyToJson(Policy policy) { - return toJson(policy); - } - - @SneakyThrows - private String toJson(Object o) { - return new ObjectMapper().writeValueAsString(o); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java deleted file mode 100644 index fa3419438..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/ConnectorApiTest.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.TestPolicy; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorDetailPageQuery; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import jakarta.json.JsonObject; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class ConnectorApiTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - } - - @Test - void testQueryConnectors() { - TEST_DATABASE.testTransaction(dsl -> { - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-1") - .title("My Asset 1") - .dataCategory("my-category") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); - - var result = brokerServerClient().brokerServerApi().connectorPage(new ConnectorPageQuery()); - assertThat(result.getConnectors()).hasSize(1); - - var connector = result.getConnectors().get(0); - assertThat(connector.getParticipantId()).isEqualTo("my-participant-id"); - assertThat(connector.getEndpoint()).isEqualTo("https://my-connector/api/dsp"); - assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); - assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); - assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); - assertThat(connector.getNumDataOffers()).isEqualTo(1); - }); - } - - @Test - void testQueryConnectorDetails() { - TEST_DATABASE.testTransaction(dsl -> { - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-1") - .title("My Asset 1") - .dataCategory("my-category") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createConnector(dsl, today, "https://my-connector2/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd); - - var connector = brokerServerClient().brokerServerApi().connectorDetailPage(new ConnectorDetailPageQuery("https://my-connector/api/dsp")); - assertThat(connector.getParticipantId()).isEqualTo("my-participant-id"); - assertThat(connector.getEndpoint()).isEqualTo("https://my-connector/api/dsp"); - assertThat(connector.getCreatedAt()).isEqualTo(today.minusDays(1)); - assertThat(connector.getLastRefreshAttemptAt()).isEqualTo(today); - assertThat(connector.getLastSuccessfulRefreshAt()).isEqualTo(today); - assertThat(connector.getConnectorCrawlingTimeAvg()).isEqualTo(150L); - }); - } - - private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { - var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setParticipantId("my-participant-id"); - connector.setEndpoint(connectorEndpoint); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(today.minusDays(1)); - connector.setLastRefreshAttemptAt(today); - connector.setLastSuccessfulRefreshAt(today); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - connector.insert(); - - addCrawlingTime(dsl, today, connector, 100L); - addCrawlingTime(dsl, today.plusHours(5), connector, 200L); - } - - private static void addCrawlingTime(DSLContext dsl, OffsetDateTime today, ConnectorRecord connector, Long duration) { - var crawlingTime = dsl.newRecord(Tables.BROKER_EXECUTION_TIME_MEASUREMENT); - crawlingTime.setConnectorEndpoint(connector.getEndpoint()); - crawlingTime.setDurationInMs(duration); - crawlingTime.setCreatedAt(today); - crawlingTime.setType(MeasurementType.CONNECTOR_REFRESH); - crawlingTime.setErrorStatus(MeasurementErrorStatus.OK); - crawlingTime.insert(); - } - - private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); - dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setCreatedAt(today.minusDays(5)); - dataOffer.setUpdatedAt(today); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); - contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(dataOffer.getAssetId()); - contractOffer.setCreatedAt(today.minusDays(5)); - contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); - contractOffer.insert(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java deleted file mode 100644 index 007ac7ffc..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DataOfferDetailApiTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageQuery; -import de.sovity.edc.ext.brokerserver.client.gen.model.DataOfferDetailPageResult; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; -import jakarta.json.JsonObject; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualUsingJson; -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayConstraint; -import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayPolicyJson; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class DataOfferDetailApiTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of( - ))); - } - - @Test - void testQueryDataOfferDetails() { - TEST_DATABASE.testTransaction(dsl -> { - var today = OffsetDateTime.now().withNano(0); - - var assetJsonLd1 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-1") - .title("My Asset 1") - .dataCategory("my-category") - .build() - ); - - var assetJsonLd2 = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id("my-asset-2") - .title("My Asset 2") - .dataCategory("my-category-2") - .build() - ); - - createConnector(dsl, today, "https://my-connector/api/dsp"); - createDataOffer(dsl, today, "https://my-connector/api/dsp", assetJsonLd1); - - createDataOfferView(dsl, today, "https://my-connector/api/dsp", "my-asset-1"); - createDataOfferView(dsl, today, "https://my-connector/api/dsp", "my-asset-1"); - - createConnector(dsl, today, "https://my-connector2/api/dsp"); - createDataOffer(dsl, today, "https://my-connector2/api/dsp", assetJsonLd2); - - var actual = brokerServerClient().brokerServerApi().dataOfferDetailPage(new DataOfferDetailPageQuery("https://my-connector/api/dsp", "my-asset-1")); - assertThat(actual.getAssetId()).isEqualTo("my-asset-1"); - assertThat(actual.getConnectorEndpoint()).isEqualTo("https://my-connector/api/dsp"); - assertThat(actual.getConnectorOfflineSinceOrLastUpdatedAt()).isEqualTo(today); - assertThat(actual.getConnectorOnlineStatus()).isEqualTo(DataOfferDetailPageResult.ConnectorOnlineStatusEnum.ONLINE); - assertThat(actual.getCreatedAt()).isEqualTo(today.minusDays(5)); - assertThat(actual.getAsset().getAssetId()).isEqualTo("my-asset-1"); - assertThat(actual.getAsset().getDataCategory()).isEqualTo("my-category"); - assertThat(actual.getAsset().getTitle()).isEqualTo("My Asset 1"); - assertThat(actual.getUpdatedAt()).isEqualTo(today); - assertThat(actual.getContractOffers()).hasSize(1); - var contractOffer = actual.getContractOffers().get(0); - assertThat(contractOffer.getContractOfferId()).isEqualTo("my-contract-offer-1"); - assertEqualUsingJson(contractOffer.getContractPolicy().getConstraints().get(0), createAfterYesterdayConstraint()); - assertThat(contractOffer.getCreatedAt()).isEqualTo(today.minusDays(5)); - assertThat(contractOffer.getUpdatedAt()).isEqualTo(today); - assertThat(actual.getViewCount()).isEqualTo(2); - }); - } - - private void createConnector(DSLContext dsl, OffsetDateTime today, String connectorEndpoint) { - var connector = dsl.newRecord(Tables.CONNECTOR); - connector.setParticipantId("my-connector"); - connector.setEndpoint(connectorEndpoint); - connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); - connector.setCreatedAt(today.minusDays(1)); - connector.setLastRefreshAttemptAt(today); - connector.setLastSuccessfulRefreshAt(today); - connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); - connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); - connector.insert(); - } - - private void createDataOffer(DSLContext dsl, OffsetDateTime today, String connectorEndpoint, JsonObject assetJsonLd) { - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - setDataOfferAssetMetadata(dataOffer, assetJsonLd, "my-participant-id"); - dataOffer.setConnectorEndpoint(connectorEndpoint); - dataOffer.setCreatedAt(today.minusDays(5)); - dataOffer.setUpdatedAt(today); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); - contractOffer.setContractOfferId("my-contract-offer-1"); - contractOffer.setConnectorEndpoint(connectorEndpoint); - contractOffer.setAssetId(dataOffer.getAssetId()); - contractOffer.setCreatedAt(today.minusDays(5)); - contractOffer.setUpdatedAt(today); - contractOffer.setPolicy(createAfterYesterdayPolicyJson()); - contractOffer.insert(); - } - - private static void createDataOfferView(DSLContext dsl, OffsetDateTime date, String connectorEndpoint, String assetId) { - var view = dsl.newRecord(Tables.DATA_OFFER_VIEW_COUNT); - view.setAssetId(assetId); - view.setConnectorEndpoint(connectorEndpoint); - view.setDate(date); - view.insert(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java deleted file mode 100644 index fb6ba3aca..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/api/DeleteConnectorsApiTest.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.api; - -import de.sovity.edc.ext.brokerserver.TestPolicy; -import de.sovity.edc.ext.brokerserver.TestUtils; -import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.BrokerEventType; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementType; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.jooq.DSLContext; -import org.jooq.Record; -import org.jooq.TableField; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.TestAsset.setDataOfferAssetMetadata; -import static de.sovity.edc.ext.brokerserver.TestUtils.ADMIN_API_KEY; -import static de.sovity.edc.ext.brokerserver.TestUtils.brokerServerClient; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - -@ApiTest -@ExtendWith(EdcExtension.class) -class DeleteConnectorsApiTest { - BrokerServerClient client; - String firstConnector = "http://a"; - String otherConnector = "http://b"; - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - client = brokerServerClient(); - } - - @Test - void testRemoveConnectors() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - setupConnectorData(dsl, firstConnector); - setupConnectorData(dsl, otherConnector); - - var connectorsBefore = List.of(firstConnector, otherConnector); - assertContainsEndpoints(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, connectorsBefore); - assertContainsEndpoints(dsl, Tables.CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); - assertContainsEndpoints(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, connectorsBefore); - assertContainsEndpoints(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, connectorsBefore); - assertContainsEndpoints(dsl, Tables.CONNECTOR.ENDPOINT, connectorsBefore); - - // act - client.brokerServerApi().deleteConnectors(ADMIN_API_KEY, List.of(firstConnector)); - - // assert - assertThat(client.brokerServerApi().connectorPage(new ConnectorPageQuery()).getConnectors()) - .extracting(ConnectorListEntry::getEndpoint) - .containsExactly(otherConnector); - - var connectorsAfter = List.of(otherConnector); - assertContainsEndpoints(dsl, Tables.BROKER_EXECUTION_TIME_MEASUREMENT.CONNECTOR_ENDPOINT, connectorsAfter); - assertContainsEndpoints(dsl, Tables.CONTRACT_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); - assertContainsEndpoints(dsl, Tables.DATA_OFFER.CONNECTOR_ENDPOINT, connectorsAfter); - assertContainsEndpoints(dsl, Tables.DATA_OFFER_VIEW_COUNT.CONNECTOR_ENDPOINT, connectorsAfter); - assertContainsEndpoints(dsl, Tables.CONNECTOR.ENDPOINT, connectorsAfter); - }); - } - - private void assertContainsEndpoints( - DSLContext dsl, - TableField endpointField, - Collection expected - ) { - var actual = dsl.select(endpointField).from(endpointField.getTable()).fetchSet(endpointField); - assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); - } - - public void setupConnectorData(DSLContext dsl, String endpoint) { - client.brokerServerApi().addConnectors(ADMIN_API_KEY, List.of(endpoint)); - - var assetId = "my-asset"; - - var dataOffer = dsl.newRecord(Tables.DATA_OFFER); - setDataOfferAssetMetadata(dataOffer, getAssetJsonLd("my-asset"), "my-participant-id"); - dataOffer.setConnectorEndpoint(endpoint); - dataOffer.setCreatedAt(OffsetDateTime.now()); - dataOffer.setUpdatedAt(OffsetDateTime.now()); - dataOffer.insert(); - - var contractOffer = dsl.newRecord(Tables.CONTRACT_OFFER); - contractOffer.setAssetId(assetId); - contractOffer.setConnectorEndpoint(endpoint); - contractOffer.setContractOfferId("my-asset-co"); - contractOffer.setCreatedAt(OffsetDateTime.now()); - contractOffer.setPolicy(TestPolicy.createAfterYesterdayPolicyJson()); - contractOffer.setUpdatedAt(OffsetDateTime.now()); - contractOffer.insert(); - - var logEntry = dsl.newRecord(Tables.BROKER_EVENT_LOG); - logEntry.setEvent(BrokerEventType.CONNECTOR_UPDATED); - logEntry.setUserMessage("Hello World!"); - logEntry.setAssetId(assetId); - logEntry.setCreatedAt(OffsetDateTime.now()); - logEntry.setConnectorEndpoint(endpoint); - logEntry.setEventStatus(BrokerEventStatus.OK); - logEntry.insert(); - - var measurement = dsl.newRecord(Tables.BROKER_EXECUTION_TIME_MEASUREMENT); - measurement.setConnectorEndpoint(endpoint); - measurement.setCreatedAt(OffsetDateTime.now()); - measurement.setDurationInMs(500L); - measurement.setErrorStatus(MeasurementErrorStatus.OK); - measurement.setType(MeasurementType.CONNECTOR_REFRESH); - measurement.insert(); - - var view = dsl.newRecord(Tables.DATA_OFFER_VIEW_COUNT); - view.setConnectorEndpoint(endpoint); - view.setAssetId(assetId); - view.setDate(OffsetDateTime.now()); - view.insert(); - } - - - @Test - void testDeleteWrongApiKey() { - TEST_DATABASE.testTransaction(dsl -> TestUtils.assertIs401(() -> - client.brokerServerApi().deleteConnectors("wrong-api-key", List.of()))); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java deleted file mode 100644 index eee984c6f..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventLoggerTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.logging; - -import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -class BrokerEventLoggerTest { - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeAll - static void setup() { - FlywayTestUtils.migrate(TEST_DATABASE); - } - - @Test - void testDataOfferWriter_allSortsOfUpdates() { - TEST_DATABASE.testTransaction(dsl -> { - var brokerEventLogger = new BrokerEventLogger(); - - // Test that insertions insert required fields and don't cause DB errors - String endpoint = "https://example.com/api/dsp"; - brokerEventLogger.logConnectorUpdated(dsl, endpoint, new ConnectorChangeTracker()); - brokerEventLogger.logConnectorOnline(dsl, endpoint); - brokerEventLogger.logConnectorOffline(dsl, endpoint, new BrokerEventErrorMessage("Message", "Stacktrace")); - brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(dsl, 10, endpoint); - brokerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, endpoint); - brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, 10, endpoint); - brokerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, endpoint); - brokerEventLogger.logConnectorsDeleted(dsl, Set.of(endpoint)); - - assertThat(numLogEntries(dsl)).isEqualTo(8); - }); - } - - private Integer numLogEntries(DSLContext dsl) { - return dsl.selectCount().from(Tables.BROKER_EVENT_LOG).fetchOne().component1(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java deleted file mode 100644 index 55b2b1efa..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolQueueTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.queue; - -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; - -import static org.assertj.core.api.Assertions.assertThat; - -class ThreadPoolQueueTest { - - - /** - * Regression against bug where the queue did not act like a queue. - */ - @Test - void testOrdering() { - Runnable noop = () -> { - }; - - var queue = new ThreadPoolTaskQueue(); - queue.add(new ThreadPoolTask(1, noop, "1.0")); - queue.add(new ThreadPoolTask(2, noop, "2.0")); - queue.add(new ThreadPoolTask(1, noop, "1.1")); - queue.add(new ThreadPoolTask(2, noop, "2.1")); - queue.add(new ThreadPoolTask(0, noop, "0.0")); - - var result = new ArrayList(); - queue.getQueue().drainTo(result); - - assertThat(result).extracting(ThreadPoolTask::getConnectorEndpoint) - .containsExactly("0.0", "1.0", "1.1", "2.0", "2.1"); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java deleted file mode 100644 index a45e23d0b..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.queue; - -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import lombok.SneakyThrows; -import org.apache.commons.lang3.Validate; -import org.eclipse.edc.spi.monitor.Monitor; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class ThreadPoolTest { - - /** - * Regression against bug where parallelity wasn't actually enabled - */ - @Test - @SneakyThrows - void testParallelExecution() { - ThreadPool threadPool = newThreadPool(2); - var latch = new CountDownLatch(2); - var result = new ArrayList(); - - var a = new BlockedRunnable(() -> { - result.add("a"); - latch.countDown(); - }); - var b = new BlockedRunnable(() -> { - result.add("b"); - latch.countDown(); - }); - - threadPool.enqueueConnectorRefreshTask(0, a, "a"); - threadPool.enqueueConnectorRefreshTask(0, b, "b"); - - b.release(); - Thread.sleep(250); // For some reason this is required - a.release(); - - Validate.isTrue(latch.await(500, TimeUnit.MILLISECONDS), "latch timed out"); - assertThat(result).containsExactly("b", "a"); - } - - - @NotNull - private ThreadPool newThreadPool(int numThreads) { - var monitor = mock(Monitor.class); - var brokerServerSettings = mock(BrokerServerSettings.class); - when(brokerServerSettings.getNumThreads()).thenReturn(numThreads); - var queue = new ThreadPoolTaskQueue(); - return new ThreadPool(queue, brokerServerSettings, monitor); - } - - private static class BlockedRunnable implements Runnable { - private static final Object GLOBAL_LOCK = new Object(); - private final Runnable runnable; - private final ReentrantLock lock = new ReentrantLock(); - - private BlockedRunnable(Runnable runnable) { - this.runnable = runnable; - lock.lock(); - } - - public void release() { - lock.unlock(); - } - - @Override - @SneakyThrows - public void run() { - var ok = lock.tryLock(10, TimeUnit.SECONDS); - Validate.isTrue(ok, "Program is stuck!"); - - // Prevent concurrency issues within the test code - synchronized (GLOBAL_LOCK) { - runnable.run(); - } - } - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java deleted file mode 100644 index 3e8b82369..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdaterTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing; - -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.client.gen.model.ContractDefinitionRequest; -import de.sovity.edc.client.gen.model.DataSourceType; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; -import de.sovity.edc.client.gen.model.UiAssetCreateRequest; -import de.sovity.edc.client.gen.model.UiCriterion; -import de.sovity.edc.client.gen.model.UiCriterionLiteral; -import de.sovity.edc.client.gen.model.UiCriterionLiteralType; -import de.sovity.edc.client.gen.model.UiCriterionOperator; -import de.sovity.edc.client.gen.model.UiDataSource; -import de.sovity.edc.client.gen.model.UiDataSourceHttpData; -import de.sovity.edc.ext.brokerserver.AssertionUtils; -import de.sovity.edc.ext.brokerserver.BrokerServerExtensionContext; -import de.sovity.edc.ext.brokerserver.TestUtils; -import de.sovity.edc.ext.brokerserver.client.BrokerServerClient; -import de.sovity.edc.ext.brokerserver.client.gen.model.CatalogPageQuery; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorListEntry; -import de.sovity.edc.ext.brokerserver.client.gen.model.ConnectorPageQuery; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.OffsetDateTime; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Map; - -import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayConstraint; -import static de.sovity.edc.ext.brokerserver.TestPolicy.createAfterYesterdayPolicyEdcGen; -import static de.sovity.edc.ext.brokerserver.TestUtils.createConfiguration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; - -@ApiTest -class ConnectorUpdaterTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @RegisterExtension - static EdcExtension consumerEdcContext = new EdcExtension(); - - private EdcClient providerClient; - - private BrokerServerClient brokerServerClient; - - @BeforeEach - void setUp(EdcExtension extension) { - extension.setConfiguration(createConfiguration(TEST_DATABASE, Map.of())); - - providerClient = EdcClient.builder() - .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) - .managementApiKey(TestUtils.MANAGEMENT_API_KEY) - .build(); - - brokerServerClient = BrokerServerClient.builder() - .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) - .managementApiKey(TestUtils.MANAGEMENT_API_KEY) - .build(); - } - - @Test - void testConnectorUpdate() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - var connectorUpdater = BrokerServerExtensionContext.instance.connectorUpdater(); - var connectorCreator = BrokerServerExtensionContext.instance.connectorCreator(); - String connectorEndpoint = TestUtils.PROTOCOL_ENDPOINT; - - var policyId = createPolicyDefinition(); - var assetId = createAsset(); - createContractDefinition(policyId, assetId); - connectorCreator.addConnector(dsl, connectorEndpoint); - - // act - connectorUpdater.updateConnector(connectorEndpoint); - var connectorPage = brokerServerClient.brokerServerApi().connectorPage(new ConnectorPageQuery()); - - // assert - var catalog = brokerServerClient.brokerServerApi().catalogPage(new CatalogPageQuery()); - assertThat(catalog.getDataOffers()).hasSize(1); - var dataOffer = catalog.getDataOffers().get(0); - assertThat(dataOffer.getContractOffers()).hasSize(1); - var contractOffer = dataOffer.getContractOffers().get(0); - var asset = dataOffer.getAsset(); - assertThat(asset.getAssetId()).isEqualTo(assetId); - assertThat(asset.getTitle()).isEqualTo("AssetName"); - assertThat(asset.getConnectorEndpoint()).isEqualTo(TestUtils.PROTOCOL_ENDPOINT); - assertThat(asset.getParticipantId()).isEqualTo(TestUtils.PARTICIPANT_ID); - assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); - assertThat(asset.getDescription()).isEqualTo("AssetDescription"); - assertThat(asset.getVersion()).isEqualTo("1.0.0"); - assertThat(asset.getLanguage()).isEqualTo("en"); - assertThat(asset.getMediaType()).isEqualTo("application/json"); - assertThat(asset.getDataCategory()).isEqualTo("dataCategory"); - assertThat(asset.getDataSubcategory()).isEqualTo("dataSubcategory"); - assertThat(asset.getDataModel()).isEqualTo("dataModel"); - assertThat(asset.getGeoReferenceMethod()).isEqualTo("geoReferenceMethod"); - assertThat(asset.getTransportMode()).isEqualTo("transportMode"); - assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url"); - assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); - assertThat(asset.getCreatorOrganizationName()).isEqualTo("Unknown"); - assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage"); - assertThat(asset.getHttpDatasourceHintsProxyMethod()).isFalse(); - assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); - assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isFalse(); - assertThat(asset.getHttpDatasourceHintsProxyBody()).isFalse(); - assertThat(asset.getCustomJsonAsString()) - .isEqualTo("{\"a\":\"x\"}"); - assertThat(dataOffer.getAsset().getCustomJsonLdAsString()) - .isEqualTo("{\"http://unknown/b\":{\"http://unknown/c\":\"y\"}}"); - assertThat(dataOffer.getAsset().getPrivateCustomJsonAsString()).isNull(); - assertThat(dataOffer.getAsset().getPrivateCustomJsonLdAsString()).isEqualTo("{}"); - var policy = contractOffer.getContractPolicy(); - assertThat(policy.getConstraints()).hasSize(1); - AssertionUtils.assertEqualUsingJson(policy.getConstraints().get(0), createAfterYesterdayConstraint()); - - var connector = connectorPage.getConnectors().get(0); - assertThat(connector.getOnlineStatus()).isEqualTo(ConnectorListEntry.OnlineStatusEnum.ONLINE); - assertThat(connector.getParticipantId()).isEqualTo(TestUtils.PARTICIPANT_ID); - assertThat(connector.getOrganizationName()).isEqualTo("Unknown"); - assertThat(connector.getLastRefreshAttemptAt()).isCloseTo(OffsetDateTime.now(), within(1, ChronoUnit.SECONDS)); - - var connectorRecord = dsl.selectFrom(Tables.CONNECTOR).fetchOne(); - assertThat(connectorRecord.getMdsId()).isEqualTo("MDSL1234ZZ"); - }); - } - - private String createPolicyDefinition() { - var policyDefinition = PolicyDefinitionCreateRequest.builder() - .policyDefinitionId("policy-1") - .policy(createAfterYesterdayPolicyEdcGen()) - .build(); - - return providerClient.uiApi().createPolicyDefinition(policyDefinition).getId(); - } - - public void createContractDefinition(String policyId, String assetId) { - providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() - .contractDefinitionId("cd-1") - .accessPolicyId(policyId) - .contractPolicyId(policyId) - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(assetId) - .build()) - .build())) - .build()); - } - - private String createAsset() { - var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl("http://some.url") - .build()) - .build(); - - return providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() - .id("asset-1") - .title("AssetName") - .description("AssetDescription") - .licenseUrl("https://license-url") - .version("1.0.0") - .language("en") - .mediaType("application/json") - .dataCategory("dataCategory") - .dataSubcategory("dataSubcategory") - .dataModel("dataModel") - .geoReferenceMethod("geoReferenceMethod") - .transportMode("transportMode") - .keywords(List.of("keyword1", "keyword2")) - .publisherHomepage("publisherHomepage") - .dataSource(dataSource) - .customJsonAsString("{\"a\":\"x\"}") - .customJsonLdAsString("{\"http://unknown/b\":{\"http://unknown/c\":\"y\"}}") - .privateCustomJsonAsString("{\"a-private\":\"x-private\"}") - .privateCustomJsonLdAsString("{\"http://unknown/b-private\":{\"http://unknown/c-private\":\"y-private\"}}") - .build()).getId(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java deleted file mode 100644 index d44383937..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDydi.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.dao.ContractOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import lombok.Value; -import org.eclipse.edc.spi.system.configuration.Config; - -import static org.mockito.Mockito.mock; - -@Value -class DataOfferWriterTestDydi { - Config config = mock(Config.class); - BrokerServerSettings brokerServerSettings = mock(BrokerServerSettings.class); - DataOfferQueries dataOfferQueries = new DataOfferQueries(); - ContractOfferQueries contractOfferQueries = new ContractOfferQueries(); - ContractOfferRecordUpdater contractOfferRecordUpdater = new ContractOfferRecordUpdater(); - DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater(); - DataOfferPatchBuilder dataOfferPatchBuilder = new DataOfferPatchBuilder( - contractOfferQueries, - dataOfferQueries, - dataOfferRecordUpdater, - contractOfferRecordUpdater - ); - DataOfferPatchApplier dataOfferPatchApplier = new DataOfferPatchApplier(); - DataOfferWriter dataOfferWriter = new DataOfferWriter(dataOfferPatchBuilder, dataOfferPatchApplier); -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java deleted file mode 100644 index feb69efc4..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRemovalJobTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.services.schedules; - -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.services.ConnectorCleaner; -import de.sovity.edc.ext.brokerserver.services.ConnectorKiller; -import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import org.jooq.DSLContext; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.time.Duration; -import java.time.OffsetDateTime; - -import static de.sovity.edc.ext.brokerserver.db.jooq.tables.Connector.CONNECTOR; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class OfflineConnectorRemovalJobTest { - - @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - BrokerServerSettings brokerServerSettings; - OfflineConnectorKiller offlineConnectorKiller; - - @BeforeAll - static void beforeAll() { - FlywayTestUtils.migrate(TEST_DATABASE); - } - - @BeforeEach - void beforeEach() { - brokerServerSettings = mock(BrokerServerSettings.class); - offlineConnectorKiller = new OfflineConnectorKiller( - brokerServerSettings, - new ConnectorQueries(), - new BrokerEventLogger(), - new ConnectorKiller(), - new ConnectorCleaner() - ); - } - - @Test - void test_offlineConnectorKiller_should_be_dead() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - when(brokerServerSettings.getKillOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); - createConnector(dsl, 6); - - // act - offlineConnectorKiller.killIfOfflineTooLong(dsl); - - // assert - dsl.select().from(CONNECTOR).fetch().forEach(record -> { - assertThat(record.get(CONNECTOR.ENDPOINT)).isEqualTo("https://my-connector/api/dsp"); - assertThat(record.get(CONNECTOR.ONLINE_STATUS)).isEqualTo(ConnectorOnlineStatus.DEAD); - }); - }); - } - - @Test - void test_offlineConnectorKiller_should_not_be_dead() { - TEST_DATABASE.testTransaction(dsl -> { - // arrange - when(brokerServerSettings.getKillOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); - createConnector(dsl, 2); - - // act - offlineConnectorKiller.killIfOfflineTooLong(dsl); - - // assert - dsl.select().from(CONNECTOR).fetch().forEach(record -> { - assertThat(record.get(CONNECTOR.ENDPOINT)).isEqualTo("https://my-connector/api/dsp"); - assertThat(record.get(CONNECTOR.ONLINE_STATUS)).isNotEqualTo(ConnectorOnlineStatus.DEAD); - }); - }); - } - - private static void createConnector(DSLContext dsl, int createdDaysAgo) { - dsl.insertInto(CONNECTOR) - .set(CONNECTOR.ENDPOINT, "https://my-connector/api/dsp") - .set(CONNECTOR.PARTICIPANT_ID, "my-connector") - .set(CONNECTOR.ONLINE_STATUS, ConnectorOnlineStatus.OFFLINE) - .set(CONNECTOR.LAST_SUCCESSFUL_REFRESH_AT, OffsetDateTime.now().minusDays(createdDaysAgo)) - .set(CONNECTOR.CREATED_AT, OffsetDateTime.now().minusDays(6)) - .set(CONNECTOR.DATA_OFFERS_EXCEEDED, ConnectorDataOffersExceeded.OK) - .set(CONNECTOR.CONTRACT_OFFERS_EXCEEDED, ConnectorContractOffersExceeded.OK).execute(); - } -} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java b/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java deleted file mode 100644 index 72a223007..000000000 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/UrlUtilsTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.brokerserver.utils; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class UrlUtilsTest { - @Test - void test_urlUtils() { - assertTrue(UrlUtils.isValidUrl("http://localhost:8080")); - assertTrue(UrlUtils.isValidUrl(" http://localhost:8080")); - - assertFalse(UrlUtils.isValidUrl("test")); - assertFalse(UrlUtils.isValidUrl("")); - assertFalse(UrlUtils.isValidUrl(" ")); - assertFalse(UrlUtils.isValidUrl(null)); - } -} diff --git a/extensions/catalog-crawler/README.md b/extensions/catalog-crawler/README.md new file mode 100644 index 000000000..28de136de --- /dev/null +++ b/extensions/catalog-crawler/README.md @@ -0,0 +1,39 @@ + +
      +

      + + Logo + + +

      EDC-Connector Extension:
      Catalog Crawler

      + +

      + Report Bug + · + Request Feature +

      +
      + +## About this Extension + +The catalog crawler is a deployment unit depending on an existing Authority Portal's database: + +- It periodically checks the Authority Portal's connector list for its environment. +- It crawls the given connectors in regular intervals. +- It writes the data offers and connector statuses back into the Authority Portal DB. +- Each Environment configured in the Authority Portal requires its own Catalog Crawler with credentials for that environment's DAPS. + +## Why does this component exist? + +The Authority Portal uses a non-EDC stack, and the EDC stack cannot handle multiple sources of authority at once. + +With the `DB -> UI` part of the broker having been moved to the Authority Portal, only the `Catalog -> DB` part remains as the Catalog Crawler, +as it requires Connector-to-Connector IAM within the given Dataspace. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts b/extensions/catalog-crawler/catalog-crawler-db/build.gradle.kts similarity index 78% rename from extensions/broker-server-postgres-flyway-jooq/build.gradle.kts rename to extensions/catalog-crawler/catalog-crawler-db/build.gradle.kts index 9320b4c62..5c97eb9c4 100644 --- a/extensions/broker-server-postgres-flyway-jooq/build.gradle.kts +++ b/extensions/catalog-crawler/catalog-crawler-db/build.gradle.kts @@ -7,9 +7,8 @@ val jooqDbType = "org.jooq.meta.postgres.PostgresDatabase" val jdbcDriver = "org.postgresql.Driver" val postgresContainer = libs.versions.postgresDbImage.get() -val migrationsDir = "src/main/resources/db/migration" -val testDataDir = "src/main/resources/db/testdata" -val jooqTargetPackage = "de.sovity.edc.ext.brokerserver.db.jooq" +val migrationsDir = "src/main/resources/db-crawler/migration" +val jooqTargetPackage = "de.sovity.edc.ext.catalog.crawler.db.jooq" val jooqTargetSourceRoot = "build/generated/jooq" val jooqTargetDir = jooqTargetSourceRoot + "/" + jooqTargetPackage.replace(".", "/") @@ -34,20 +33,9 @@ dependencies { jooqGenerator(libs.postgres) flywayMigration(libs.postgres) - implementation(libs.hikari) - - annotationProcessor(libs.lombok) - compileOnly(libs.lombok) - implementation(libs.apache.commonsLang) - - implementation(libs.edc.coreSpi) // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) implementation(libs.postgres) - - api(libs.flyway.core) - - testImplementation(libs.edc.junit) } sourceSets { @@ -71,20 +59,20 @@ fun isTestcontainersEnabled(): Boolean { fun jdbcUrl(): String { return container?.jdbcUrl - ?: System.getenv()[jdbcUrlEnvVarName] - ?: error("Need $jdbcUrlEnvVarName since $skipTestcontainersEnvVarName=true") + ?: System.getenv()[jdbcUrlEnvVarName] + ?: error("Need $jdbcUrlEnvVarName since $skipTestcontainersEnvVarName=true") } fun jdbcUser(): String { return container?.username - ?: System.getenv()[jdbcUserEnvVarName] - ?: error("Need $jdbcUserEnvVarName since $skipTestcontainersEnvVarName=true") + ?: System.getenv()[jdbcUserEnvVarName] + ?: error("Need $jdbcUserEnvVarName since $skipTestcontainersEnvVarName=true") } fun jdbcPassword(): String { return container?.password - ?: System.getenv()[jdbcPasswordEnvVarName] - ?: error("Need $jdbcPasswordEnvVarName since $skipTestcontainersEnvVarName=true") + ?: System.getenv()[jdbcPasswordEnvVarName] + ?: error("Need $jdbcPasswordEnvVarName since $skipTestcontainersEnvVarName=true") } @@ -107,8 +95,9 @@ flyway { cleanDisabled = false cleanOnValidationError = true baselineOnMigrate = true - locations = arrayOf("filesystem:${migrationsDir}", "filesystem:${testDataDir}") + locations = arrayOf("filesystem:${migrationsDir}") configurations = arrayOf("flywayMigration") + failOnMissingLocations = true mixed = true } @@ -159,15 +148,15 @@ jooq { tasks.withType { dependsOn.add("flywayMigrate") inputs.files(fileTree(migrationsDir)) - .withPropertyName("migrations") - .withPathSensitivity(PathSensitivity.RELATIVE) + .withPropertyName("migrations") + .withPathSensitivity(PathSensitivity.RELATIVE) allInputsDeclared.set(true) outputs.cacheIf { true } doFirst { require(this is nu.studer.gradle.jooq.JooqGenerate) val jooqConfiguration = nu.studer.gradle.jooq.JooqGenerate::class.java.getDeclaredField("jooqConfiguration") - .also { it.isAccessible = true }.get(this) as org.jooq.meta.jaxb.Configuration + .also { it.isAccessible = true }.get(this) as org.jooq.meta.jaxb.Configuration jooqConfiguration.jdbc.apply { url = jdbcUrl() @@ -180,7 +169,7 @@ tasks.withType { } } -group = libs.versions.sovityBrokerServerGroup.get() +group = libs.versions.sovityCatalogCrawlerGroup.get() publishing { publications { diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration-test-utils/V9999__Make_Columns_Nullable.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration-test-utils/V9999__Make_Columns_Nullable.sql new file mode 100644 index 000000000..bc4f9fd8c --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration-test-utils/V9999__Make_Columns_Nullable.sql @@ -0,0 +1,25 @@ +do +$$ + declare + r record; + begin + for r in (select 'alter table "' || c.table_schema || '"."' || c.table_name || '" alter column "' || c.column_name || + '" drop not null;' as command + from information_schema.columns c + where c.table_schema not in ('pg_catalog', 'information_schema') -- exclude system schemas + and c.table_name in ('connector', 'organization', 'user') -- only selected AP tables + and c.is_nullable = 'NO' + and not exists (SELECT tc.constraint_type + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema = c.table_schema + and tc.table_name = c.table_name + AND kcu.column_name = c.column_name + AND tc.constraint_type = 'PRIMARY KEY')) -- exclude primary keys + loop + execute r.command; + end loop; + end +$$; diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V1__Initial_Schema.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V1__Initial_Schema.sql new file mode 100644 index 000000000..bd792737a --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V1__Initial_Schema.sql @@ -0,0 +1,50 @@ +-- User +create type user_registration_status as enum ('FIRST_USER', 'CREATED', 'PENDING', 'APPROVED', 'REJECTED'); + +create table "user" +( + id text not null primary key, + organization_mds_id text, + registration_status user_registration_status not null +); + +-- Organization +create type organization_registration_status as enum ('PENDING', 'APPROVED', 'REJECTED'); + +create table "organization" +( + mds_id text not null primary key, + name text not null, + address text not null, + duns text not null, + url text not null, + security_email text not null, + created_by text not null, + registration_status organization_registration_status not null, + constraint fk_organization_created_by foreign key (created_by) references "user" (id) +); + +alter table "user" + add constraint fk_user_organization_id + foreign key (organization_mds_id) references "organization" (mds_id); + +-- Connector +create type connector_type as enum ('OWN', 'PROVIDED', 'CAAS'); + +create table "connector" +( + connector_id text not null primary key, + mds_id text not null, + provider_mds_id text not null, + type connector_type not null, + environment text not null, + client_id text not null, + name text not null, + location text not null, + url text not null, + created_by text not null, + created_at timestamp with time zone not null, + constraint fk_connector_organization_id foreign key (mds_id) references "organization" (mds_id), + constraint fk_connector_provider_id foreign key (provider_mds_id) references "organization" (mds_id), + constraint fk_connector_created_by foreign key (created_by) references "user" (id) +); diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V2__Schema_Extension.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V2__Schema_Extension.sql new file mode 100644 index 000000000..599d64773 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V2__Schema_Extension.sql @@ -0,0 +1,12 @@ +-- User +alter type user_registration_status add value 'INVITED' before 'CREATED'; +alter type user_registration_status add value 'DEACTIVATED' after 'REJECTED'; +alter type user_registration_status rename value 'APPROVED' to 'ACTIVE'; + +alter table "user" add column created_at timestamp with time zone not null default now(); + +-- Organization +alter type organization_registration_status add value 'INVITED' before 'PENDING'; +alter type organization_registration_status rename value 'APPROVED' to 'ACTIVE'; + +alter table "organization" add column created_at timestamp with time zone not null default now(); diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V3__Schema_Extension.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V3__Schema_Extension.sql new file mode 100644 index 000000000..e9aafee70 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V3__Schema_Extension.sql @@ -0,0 +1,28 @@ +-- User +ALTER TABLE "user" + ADD COLUMN email text, + ADD COLUMN first_name text, + ADD COLUMN last_name text, + ADD COLUMN job_title text, + ADD COLUMN phone text; + +-- Organization +ALTER TABLE "organization" + ADD COLUMN business_unit text, + ADD COLUMN billing_address text, + ADD COLUMN tax_id text, + ADD COLUMN commerce_register_number text, + ADD COLUMN commerce_register_location text, + ADD COLUMN main_contact_name text, + ADD COLUMN main_contact_email text, + ADD COLUMN main_contact_phone text, + ADD COLUMN tech_contact_name text, + ADD COLUMN tech_contact_email text, + ADD COLUMN tech_contact_phone text; + +UPDATE "organization" SET main_contact_email = security_email; + +ALTER TABLE "organization" + DROP COLUMN duns, + DROP COLUMN security_email; + diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V4__Schema_Extension.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V4__Schema_Extension.sql new file mode 100644 index 000000000..bfe8162fa --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V4__Schema_Extension.sql @@ -0,0 +1,14 @@ +-- Components +create table "component" ( + id text primary key, + mds_id text not null, + name text not null, + homepage_url text, + endpoint_url text not null, + environment text not null, + client_id text not null, + created_by text not null, + created_at timestamp with time zone not null default now(), + constraint fk_component_created_by foreign key (created_by) references "user" (id), + constraint fk_component_organization_id foreign key (mds_id) references "organization" (mds_id) +); diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V5__Schema_Extension.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V5__Schema_Extension.sql new file mode 100644 index 000000000..ec89fa6bb --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V5__Schema_Extension.sql @@ -0,0 +1,46 @@ +-- User +create type user_onboarding_type as enum ('INVITATION', 'SELF_REGISTRATION'); + +alter table "user" + add column onboarding_type user_onboarding_type, + add column invited_by text, + add constraint fk_invited_by foreign key (invited_by) references "user" (id); + +update "user" set onboarding_type = 'SELF_REGISTRATION' where onboarding_type is null; + +-- Connector +create type connector_broker_registration_status as enum ('REGISTERED', 'UNREGISTERED'); +create type caas_status as enum ('INIT', 'PROVISIONING', 'AWAITING_RUNNING', 'RUNNING', 'DEPROVISIONING', 'AWAITING_STOPPED', 'STOPPED', 'ERROR', 'NOT_FOUND'); + +alter table "connector" + add column broker_registration_status connector_broker_registration_status not null default 'UNREGISTERED', + add column management_url text, + add column endpoint_url text, + add column jwks_url text, + add column caas_status caas_status, + alter column provider_mds_id drop not null, + alter column url drop not null; + +alter table "connector" + rename column url to frontend_url; + +-- Fallback in case someone tries to migrate from 0.x to 1.0 +update "connector" +set management_url = frontend_url || '/api/management' where management_url is null; + +update "connector" +set endpoint_url = frontend_url || '/api/dsp' where endpoint_url is null; + +-- Organization id type +create type organization_legal_id_type as enum ('TAX_ID', 'COMMERCE_REGISTER_INFO'); +alter table "organization" + add column legal_id_type organization_legal_id_type, + add column description text, + alter column address drop not null, + alter column url drop not null; +update organization set legal_id_type = 'TAX_ID' where organization.tax_id is not null; +update organization set legal_id_type = 'COMMERCE_REGISTER_INFO' where organization.commerce_register_number is not null and tax_id is null; + +-- New registration flow +alter type user_registration_status add value 'ONBOARDING' after 'CREATED'; +alter type organization_registration_status add value 'ONBOARDING' after 'INVITED'; diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V6__AP_Release_1_0_0.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V6__AP_Release_1_0_0.sql new file mode 100644 index 000000000..1aef70850 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V6__AP_Release_1_0_0.sql @@ -0,0 +1,39 @@ +-- Edit Enum: Remove UserRegistrationStatus.CREATED, UserRegistrationStatus.FIRST_USER +delete +from "user" +where registration_status = 'CREATED' + or registration_status = 'FIRST_USER'; + +create type tmp_enum AS ENUM ('INVITED', 'ONBOARDING', 'PENDING', 'ACTIVE', 'REJECTED', 'DEACTIVATED'); + +alter table "user" + alter column registration_status type tmp_enum + using (registration_status::text::tmp_enum); + +drop type user_registration_status; + +alter type tmp_enum rename to user_registration_status; + +-- Component & Connector online status +create type component_type as enum ('BROKER', 'DAPS', 'LOGGING_HOUSE'); +create type component_online_status as enum ('UP', 'DOWN', 'PENDING', 'MAINTENANCE'); +create table "component_downtimes" +( + component component_type not null, + status component_online_status not null, + environment text not null, + time_stamp timestamp with time zone not null, + primary key (component, environment, time_stamp) +); +create index component_downtimes_time_stamp_index on "component_downtimes" (time_stamp); + +create type connector_uptime_status as enum ('UP', 'DOWN', 'DEAD'); +create table "connector_downtimes" +( + connector_id text not null, + status connector_uptime_status not null, + environment text not null, + time_stamp timestamp with time zone not null, + primary key (connector_id, time_stamp) +); +create index connector_downtimes_time_stamp_index on "connector_downtimes" (time_stamp); diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V7__Schema_Extension.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V7__Schema_Extension.sql new file mode 100644 index 000000000..0818d9c03 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V7__Schema_Extension.sql @@ -0,0 +1,3 @@ +-- Organization +alter table "organization" + add column industry text; diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V8__Schema_Extension.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V8__Schema_Extension.sql new file mode 100644 index 000000000..4a9e7b810 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V8__Schema_Extension.sql @@ -0,0 +1,4 @@ +-- Fix invalid DB state +delete +from "user" +where organization_mds_id is null; diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql new file mode 100644 index 000000000..fe46c6525 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V9__Broker_Integration.sql @@ -0,0 +1,107 @@ +create collation if not exists alphanumeric_with_natural_sort (provider = icu, locale = 'en-u-kn-true'); + +-- Creating missing enums +create type connector_online_status as enum ('ONLINE', 'OFFLINE', 'DEAD'); +create type connector_data_offers_exceeded as enum ('OK', 'EXCEEDED'); +create type connector_contract_offers_exceeded as enum ('OK', 'EXCEEDED'); +create type measurement_type as enum ('CONNECTOR_REFRESH'); +create type measurement_error_status as enum ('ERROR', 'OK'); +create type crawler_event_status as enum ('OK', 'ERROR'); +create type crawler_event_type as enum ( + 'CONNECTOR_UPDATED', + 'CONNECTOR_STATUS_CHANGE_ONLINE', + 'CONNECTOR_STATUS_CHANGE_OFFLINE', + 'CONNECTOR_STATUS_CHANGE_FORCE_DELETED', + 'CONTRACT_OFFER_UPDATED', + 'CONTRACT_OFFER_CLICK', + 'CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED', + 'CONNECTOR_DATA_OFFER_LIMIT_OK', + 'CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED', + 'CONNECTOR_CONTRACT_OFFER_LIMIT_OK', + 'CONNECTOR_KILLED_DUE_TO_OFFLINE_FOR_TOO_LONG', + 'CONNECTOR_DELETED'); + +-- Adding missing columns from the crawler to the connector table +alter table connector + add column last_refresh_attempt_at timestamp with time zone, + add column last_successful_refresh_at timestamp with time zone, + add column online_status connector_online_status not null default 'OFFLINE', + add column data_offers_exceeded connector_data_offers_exceeded not null default 'OK', + add column contract_offers_exceeded connector_contract_offers_exceeded not null default 'OK'; + +-- Data offers, additionally keyed by env ID +create table data_offer +( + connector_id text not null, + asset_id text not null, + ui_asset_json jsonb not null, + created_at timestamp with time zone not null, + updated_at timestamp with time zone, + asset_title text collate alphanumeric_with_natural_sort not null, + description text not null default ''::text, + curator_organization_name text not null default ''::text, + data_category text not null default ''::text, + data_subcategory text not null default ''::text, + data_model text not null default ''::text, + transport_mode text not null default ''::text, + geo_reference_method text not null default ''::text, + keywords text[] not null default '{}'::text[], + keywords_comma_joined text not null default ''::text, + version text not null default ''::text, + primary key (connector_id, asset_id), + constraint data_offer_connector_fkey foreign key (connector_id) references connector (connector_id) +); + +-- Data offer Viewcount +create table data_offer_view_count +( + id serial primary key, + connector_id text not null, + asset_id text not null, + date timestamp with time zone not null +); + +create index data_offer_view_count_speedup on data_offer_view_count (connector_id, asset_id); + +-- Contract offers, additionally keyed by env ID +create table contract_offer +( + contract_offer_id text not null, + connector_id text not null, + asset_id text not null, + ui_policy_json jsonb not null, + created_at timestamp with time zone not null, + updated_at timestamp with time zone, + primary key (contract_offer_id, connector_id, asset_id), + constraint contract_offer_connector_fkey foreign key (connector_id) references connector (connector_id), + constraint contract_offer_data_offer_fkey foreign key (connector_id, asset_id) references data_offer (connector_id, asset_id) +); + +-- Event Log +create table crawler_event_log +( + id uuid not null primary key, + environment text not null, + created_at timestamp with time zone not null, + user_message text not null, + event crawler_event_type not null, + event_status crawler_event_status not null, + connector_id text, + asset_id text, + error_stack text +); +create index crawler_event_log_speedup on crawler_event_log (environment, connector_id, asset_id, event_status); + +-- Crawling exec time measurements +create table crawler_execution_time_measurement +( + id uuid not null primary key, + environment text not null, + connector_id text not null, + created_at timestamp with time zone not null, + duration_in_ms bigint not null, + type measurement_type not null, + error_status measurement_error_status not null +); + + diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts b/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts new file mode 100644 index 000000000..6cc40c67d --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + `java-library` +} + +dependencies { + compileOnly(project(":launchers:connectors:catalog-crawler-dev")) + compileOnly(project(":launchers:connectors:sovity-dev")) + + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + + testImplementation(project(":utils:versions")) + testImplementation(project(":utils:test-connector-remote")) + testImplementation(project(":utils:json-and-jsonld-utils")) + testImplementation(project(":extensions:catalog-crawler:catalog-crawler-db")) + testImplementation(project(":extensions:wrapper:clients:java-client")) + testImplementation(project(":extensions:catalog-crawler:catalog-crawler")) + + testImplementation(libs.assertj.core) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.flyway.core) + testImplementation(libs.testcontainers.junitJupiter) + testImplementation(libs.testcontainers.postgresql) + testImplementation(libs.junit.api) + testImplementation(libs.jsonAssert) + testRuntimeOnly(libs.junit.engine) +} + +tasks.getByName("test") { + useJUnitPlatform() + maxParallelForks = 1 +} + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java new file mode 100644 index 000000000..3fedd0961 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.catalog.crawler; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.DataSourceType; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; +import de.sovity.edc.client.gen.model.UiPolicyConstraint; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyLiteral; +import de.sovity.edc.client.gen.model.UiPolicyLiteralType; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.utils.CrawlerDbAccess; +import de.sovity.edc.ext.catalog.crawler.utils.TestData; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.awaitility.Awaitility; +import org.awaitility.core.ThrowingRunnable; +import org.jooq.DSLContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.getFreePortRange; +import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiConfigFactory.configureApi; +import static org.assertj.core.api.Assertions.assertThat; + +class CrawlerE2eTest { + private static ConnectorConfig connectorConfig; + private static EdcClient connectorClient; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + connectorConfig = forTestDatabase("MDSL1234XX.C1234XX", testDatabase); + connectorClient = EdcClient.builder() + .managementApiUrl(connectorConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(connectorConfig.getProperties().get("edc.api.auth.key")) + .build(); + return connectorConfig.getProperties(); + } + ); + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase crawlerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:catalog-crawler-dev", + "crawler", + testDatabase -> { + var firstPort = getFreePortRange(5); + + var props = new HashMap(); + props.put("edc.participant.id", "broker"); + props.put(CrawlerExtension.EXTENSION_ENABLED, "true"); + props.put(CrawlerExtension.ENVIRONMENT_ID, "test"); + props.put(CrawlerExtension.JDBC_URL, testDatabase.getJdbcCredentials().jdbcUrl()); + props.put(CrawlerExtension.JDBC_USER, testDatabase.getJdbcCredentials().jdbcUser()); + props.put(CrawlerExtension.JDBC_PASSWORD, testDatabase.getJdbcCredentials().jdbcPassword()); + props.put(CrawlerExtension.DB_CONNECTION_POOL_SIZE, "30"); + props.put(CrawlerExtension.DB_CONNECTION_TIMEOUT_IN_MS, "1000"); + props.put(CrawlerExtension.DB_MIGRATE, "true"); + props.put(CrawlerExtension.DB_CLEAN, "true"); + props.put(CrawlerExtension.DB_CLEAN_ENABLED, "true"); + props.put(CrawlerExtension.DB_ADDITIONAL_FLYWAY_MIGRATION_LOCATIONS, "classpath:db-crawler/migration-test-utils"); + props.put(CrawlerExtension.NUM_THREADS, "2"); + props.put(CrawlerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, "100"); + props.put(CrawlerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER, "100"); + props.putAll(configureApi(firstPort, "managementApiKey").getProperties()); + + var everySeconds = "* * * * * ?"; + props.put(CrawlerExtension.CRON_ONLINE_CONNECTOR_REFRESH, everySeconds); + props.put(CrawlerExtension.CRON_OFFLINE_CONNECTOR_REFRESH, everySeconds); + props.put(CrawlerExtension.CRON_DEAD_CONNECTOR_REFRESH, everySeconds); + props.put(CrawlerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, everySeconds); + props.put(CrawlerExtension.KILL_OFFLINE_CONNECTORS_AFTER, "P1D"); + + return props; + } + ); + + private final String dataOfferId = "my-data-offer"; + + @Test + void crawlSingleDataOffer() { + // arrange + createPolicy(); + createAsset(); + createContractDefinition(); + + var connectorRef = new ConnectorRef( + "MDSL1234XX.C1234XX", + "test", + "My Org", + "MDSL1234XX", + connectorConfig.getProtocolEndpoint().getUri().toString() + ); + + crawlerTransaction(dsl -> { + TestData.insertConnector(dsl, connectorRef, connectorRecord -> { + connectorRecord.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + connectorRecord.setCreatedAt(OffsetDateTime.now()); + }); + }); + + // act / await crawl + Awaitility.await().atMost(10, TimeUnit.SECONDS) + .untilAsserted(mapExceptionsToAssertionError(() -> + crawlerTransaction(dsl -> assertDataOfferInCatalog(dsl, connectorRef)))); + } + + private void assertDataOfferInCatalog(DSLContext dsl, ConnectorRef connectorRef) { + var c = Tables.CONNECTOR; + var connector = dsl.fetchOne(c, c.CONNECTOR_ID.eq(connectorRef.getConnectorId())); + assertThat(connector).isNotNull(); + assertThat(connector.getOnlineStatus()).isEqualTo(ConnectorOnlineStatus.ONLINE); + + var d = Tables.DATA_OFFER; + var dataOffers = dsl.fetch(d, d.CONNECTOR_ID.eq(connectorRef.getConnectorId())); + assertThat(dataOffers).hasSize(1); + assertThat(dataOffers.get(0).getAssetId()).isEqualTo(dataOfferId); + assertThat(dataOffers.get(0).getAssetTitle()).isEqualTo("My Data Offer"); + } + + private void createAsset() { + var asset = UiAssetCreateRequest.builder() + .id(dataOfferId) + .title("My Data Offer") + .description("Example Data Offer.") + .version("2023-11") + .language("EN") + .publisherHomepage("https://my-department.my-org.com/my-data-offer") + .licenseUrl("https://my-department.my-org.com/my-data-offer#license") + .dataSource(UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://0.0.0.0") + .build()) + .build()) + .build(); + + connectorClient.uiApi().createAsset(asset); + } + + private void createPolicy() { + var afterYesterday = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().minusDays(1).toString()) + .build()) + .build(); + + var beforeTomorrow = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.LT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().plusDays(1).toString()) + .build()) + .build(); + + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(dataOfferId) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(afterYesterday, beforeTomorrow)) + .build()) + .build(); + + connectorClient.uiApi().createPolicyDefinition(policyDefinition); + } + + private void createContractDefinition() { + var contractDefinition = ContractDefinitionRequest.builder() + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); + + connectorClient.uiApi().createContractDefinition(contractDefinition); + } + + private void crawlerTransaction(Consumer withDsl) { + CrawlerDbAccess.transaction(crawlerExtension.getTestDatabase(), withDsl); + } + + private ThrowingRunnable mapExceptionsToAssertionError(ThrowingRunnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Exception e) { + throw new AssertionError(e); + } + }; + } +} diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CrawlerDbAccess.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CrawlerDbAccess.java new file mode 100644 index 000000000..3dd2751db --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CrawlerDbAccess.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.utils; + +import de.sovity.edc.extension.e2e.db.TestDatabase; +import lombok.experimental.UtilityClass; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; + +import java.util.function.Consumer; + +@UtilityClass +public class CrawlerDbAccess { + + public static void transaction(TestDatabase testDatabase, Consumer consumer) { + var credentials = testDatabase.getJdbcCredentials(); + try (var dslContext = DSL.using(credentials.jdbcUrl(), credentials.jdbcUser(), credentials.jdbcPassword())) { + dslContext.transaction(configuration -> { + var dsl = DSL.using(configuration); + consumer.accept(dsl); + }); + } + } +} diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java new file mode 100644 index 000000000..77e056a98 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.utils; + +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; +import lombok.experimental.UtilityClass; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.util.function.Consumer; + +@UtilityClass +public class TestData { + + public static void insertConnector( + DSLContext dsl, + ConnectorRef connectorRef, + Consumer applier + ) { + var organization = dsl.newRecord(Tables.ORGANIZATION); + organization.setMdsId(connectorRef.getOrganizationId()); + organization.setName(connectorRef.getOrganizationLegalName()); + organization.insert(); + + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setEnvironment(connectorRef.getEnvironmentId()); + connector.setMdsId(connectorRef.getOrganizationId()); + connector.setConnectorId(connectorRef.getConnectorId()); + connector.setName(connectorRef.getConnectorId() + " Name"); + connector.setEndpointUrl(connectorRef.getEndpoint()); + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + connector.setLastRefreshAttemptAt(null); + connector.setLastSuccessfulRefreshAt(null); + connector.setCreatedAt(OffsetDateTime.now()); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + applier.accept(connector); + connector.insert(); + } +} diff --git a/extensions/broker-server/src/test/resources/logging.properties b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/resources/logging.properties similarity index 88% rename from extensions/broker-server/src/test/resources/logging.properties rename to extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/resources/logging.properties index d2212b2a2..471bd20d6 100644 --- a/extensions/broker-server/src/test/resources/logging.properties +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/resources/logging.properties @@ -3,4 +3,4 @@ org.eclipse.edc.level=ALL handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.ConsoleHandler.level=ALL -java.util.logging.SimpleFormatter.format=[%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS] [%4$-7s] %5$s%6$s%n \ No newline at end of file +java.util.logging.SimpleFormatter.format=[%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS] [%4$-7s] %5$s%6$s%n diff --git a/extensions/catalog-crawler/catalog-crawler-launcher-base/build.gradle.kts b/extensions/catalog-crawler/catalog-crawler-launcher-base/build.gradle.kts new file mode 100644 index 000000000..2c376a085 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-launcher-base/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` +} + +dependencies { + // A minimal EDC that can request catalogs + api(libs.edc.controlPlaneCore) + api(libs.edc.dataPlaneSelectorCore) + api(libs.edc.configurationFilesystem) + api(libs.edc.controlPlaneAggregateServices) + api(libs.edc.http) + api(libs.edc.dsp) + api(libs.edc.jsonLd) + + // Data Catalog Crawler + api(project(":extensions:catalog-crawler:catalog-crawler")) +} + +group = libs.versions.sovityEdcGroup.get() diff --git a/extensions/broker-server/build.gradle.kts b/extensions/catalog-crawler/catalog-crawler/build.gradle.kts similarity index 55% rename from extensions/broker-server/build.gradle.kts rename to extensions/catalog-crawler/catalog-crawler/build.gradle.kts index c53445e15..b63b7004f 100644 --- a/extensions/broker-server/build.gradle.kts +++ b/extensions/catalog-crawler/catalog-crawler/build.gradle.kts @@ -2,65 +2,44 @@ plugins { `java-library` } -configurations.all { - resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) -} - dependencies { annotationProcessor(libs.lombok) compileOnly(libs.lombok) - implementation(libs.apache.commonsLang) - - api(project(":utils:catalog-parser")) - api(project(":utils:json-and-jsonld-utils")) - api(project(":extensions:wrapper:wrapper-common-mappers")) - implementation(libs.edc.controlPlaneSpi) implementation(libs.edc.managementApiConfiguration) - api(project(":extensions:broker-server-postgres-flyway-jooq")) - implementation(project(":extensions:broker-server-api:api")) + implementation(libs.quartz.quartz) + implementation(libs.apache.commonsLang) implementation(project(":utils:versions")) - implementation(libs.okhttp.okhttp) + api(project(":utils:catalog-parser")) + api(project(":utils:json-and-jsonld-utils")) + api(project(":extensions:wrapper:wrapper-common-mappers")) + api(project(":extensions:catalog-crawler:catalog-crawler-db")) + api(project(":extensions:postgres-flyway-core")) testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) - testImplementation(project(":extensions:wrapper:clients:java-client")) - testImplementation(project(":extensions:sovity-edc-extensions-package")) + testImplementation(project(":utils:test-connector-remote")) testImplementation(libs.assertj.core) testImplementation(libs.mockito.core) testImplementation(libs.mockito.inline) - testImplementation(libs.edc.controlPlaneCore) - testImplementation(libs.edc.dataPlaneSelectorCore) - testImplementation(libs.edc.junit) - testImplementation(libs.edc.http) - testImplementation(libs.edc.iamMock) - testImplementation(libs.edc.dsp) - testImplementation(libs.edc.jsonLd) - testImplementation(libs.edc.monitorJdkLogger) - testImplementation(libs.edc.configurationFilesystem) - testImplementation(project(":extensions:broker-server-api:client")) testImplementation(libs.restAssured.restAssured) testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.flyway.core) testImplementation(libs.testcontainers.junitJupiter) testImplementation(libs.testcontainers.postgresql) testImplementation(libs.junit.api) testImplementation(libs.jsonAssert) testRuntimeOnly(libs.junit.engine) - - implementation(libs.quartz.quartz) } tasks.getByName("test") { useJUnitPlatform() + maxParallelForks = 1 } -tasks.register("prepareKotlinBuildScriptModel") {} - -group = libs.versions.sovityBrokerServerGroup.get() - publishing { publications { create(project.name) { diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java new file mode 100644 index 000000000..290c5c2c3 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler; + +import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; + +import static de.sovity.edc.ext.catalog.crawler.orchestration.config.EdcConfigPropertyUtils.toEdcProp; + +@Provides({CrawlerExtensionContext.class}) +public class CrawlerExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "Authority Portal Data Catalog Crawler"; + + @Setting(required = true) + public static final String EXTENSION_ENABLED = toEdcProp("CRAWLER_EXTENSION_ENABLED"); + + @Setting(required = true) + public static final String ENVIRONMENT_ID = toEdcProp("CRAWLER_ENVIRONMENT_ID"); + + @Setting(required = true) + public static final String JDBC_URL = toEdcProp("CRAWLER_DB_JDBC_URL"); + + @Setting(required = true) + public static final String JDBC_USER = toEdcProp("CRAWLER_DB_JDBC_USER"); + + @Setting(required = true) + public static final String JDBC_PASSWORD = toEdcProp("CRAWLER_DB_JDBC_PASSWORD"); + + @Setting + public static final String DB_CONNECTION_POOL_SIZE = toEdcProp("CRAWLER_DB_CONNECTION_POOL_SIZE"); + + @Setting + public static final String DB_CONNECTION_TIMEOUT_IN_MS = toEdcProp("CRAWLER_DB_CONNECTION_TIMEOUT_IN_MS"); + + @Setting + public static final String DB_MIGRATE = toEdcProp("CRAWLER_DB_MIGRATE"); + + @Setting + public static final String DB_CLEAN = toEdcProp("CRAWLER_DB_CLEAN"); + + @Setting + public static final String DB_CLEAN_ENABLED = toEdcProp("CRAWLER_DB_CLEAN_ENABLED"); + + @Setting + public static final String DB_ADDITIONAL_FLYWAY_MIGRATION_LOCATIONS = toEdcProp("CRAWLER_DB_ADDITIONAL_FLYWAY_LOCATIONS"); + + @Setting + public static final String NUM_THREADS = toEdcProp("CRAWLER_NUM_THREADS"); + + @Setting + public static final String MAX_DATA_OFFERS_PER_CONNECTOR = toEdcProp("CRAWLER_MAX_DATA_OFFERS_PER_CONNECTOR"); + + @Setting + public static final String MAX_CONTRACT_OFFERS_PER_DATA_OFFER = toEdcProp("CRAWLER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER"); + + @Setting + public static final String CRON_ONLINE_CONNECTOR_REFRESH = toEdcProp("CRAWLER_CRON_ONLINE_CONNECTOR_REFRESH"); + + @Setting + public static final String CRON_OFFLINE_CONNECTOR_REFRESH = toEdcProp("CRAWLER_CRON_OFFLINE_CONNECTOR_REFRESH"); + + @Setting + public static final String CRON_DEAD_CONNECTOR_REFRESH = toEdcProp("CRAWLER_CRON_DEAD_CONNECTOR_REFRESH"); + + @Setting + public static final String SCHEDULED_KILL_OFFLINE_CONNECTORS = toEdcProp("CRAWLER_SCHEDULED_KILL_OFFLINE_CONNECTORS"); + @Setting + public static final String KILL_OFFLINE_CONNECTORS_AFTER = toEdcProp("CRAWLER_KILL_OFFLINE_CONNECTORS_AFTER"); + + @Inject + private TypeManager typeManager; + + @Inject + private ManagementApiTypeTransformerRegistry typeTransformerRegistry; + + @Inject + private JsonLd jsonLd; + + @Inject + private CatalogService catalogService; + + /** + * Manual Dependency Injection Result + */ + private CrawlerExtensionContext services; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + if (!Boolean.TRUE.equals(context.getConfig().getBoolean(EXTENSION_ENABLED, false))) { + context.getMonitor().info("Crawler extension is disabled."); + return; + } + + services = CrawlerExtensionContextBuilder.buildContext( + context.getConfig(), + context.getMonitor(), + typeManager, + typeTransformerRegistry, + jsonLd, + catalogService + ); + + // Provide access for the tests + context.registerService(CrawlerExtensionContext.class, services); + } + + @Override + public void start() { + if (services == null) { + return; + } + services.crawlerInitializer().onStartup(); + } + + @Override + public void shutdown() { + if (services == null) { + return; + } + services.dataSource().close(); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContext.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContext.java new file mode 100644 index 000000000..264831fe8 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContext.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler; + +import com.zaxxer.hikari.HikariDataSource; +import de.sovity.edc.ext.catalog.crawler.crawling.ConnectorCrawler; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.FetchedCatalogBuilder; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferRecordUpdater; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; + + +/** + * Manual Dependency Injection result + * + * @param crawlerInitializer Startup Logic + */ +public record CrawlerExtensionContext( + CrawlerInitializer crawlerInitializer, + // Required for stopping connections on closing + HikariDataSource dataSource, + DslContextFactory dslContextFactory, + + // Required for Integration Tests + ConnectorCrawler connectorCrawler, + PolicyMapper policyMapper, + FetchedCatalogBuilder catalogPatchBuilder, + DataOfferRecordUpdater dataOfferRecordUpdater +) { +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java new file mode 100644 index 000000000..cb49d40a5 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import de.sovity.edc.ext.catalog.crawler.crawling.ConnectorCrawler; +import de.sovity.edc.ext.catalog.crawler.crawling.OfflineConnectorCleaner; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.FetchedCatalogBuilder; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.FetchedCatalogMappingUtils; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.FetchedCatalogService; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerExecutionTimeLogger; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.CatalogPatchBuilder; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.ConnectorUpdateCatalogWriter; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.ConnectorUpdateFailureWriter; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.ConnectorUpdateSuccessWriter; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.DataOfferLimitsEnforcer; +import de.sovity.edc.ext.catalog.crawler.dao.CatalogCleaner; +import de.sovity.edc.ext.catalog.crawler.dao.CatalogPatchApplier; +import de.sovity.edc.ext.catalog.crawler.dao.config.DataSourceFactory; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.dao.config.FlywayService; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorStatusUpdater; +import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferQueries; +import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferRecordUpdater; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferQueries; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferRecordUpdater; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfigFactory; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorQueue; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ThreadPool; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ThreadPoolTaskQueue; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.DeadConnectorRefreshJob; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.OfflineConnectorCleanerJob; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.OfflineConnectorRefreshJob; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.OnlineConnectorRefreshJob; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.QuartzScheduleInitializer; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.utils.CronJobRef; +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdParser; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.DataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; +import de.sovity.edc.utils.catalog.DspCatalogService; +import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; +import lombok.NoArgsConstructor; +import org.eclipse.edc.connector.spi.catalog.CatalogService; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.CoreConstants; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + + +/** + * Manual Dependency Injection (DYDI). + *

      + * We want to develop as Java Backend Development is done, but we have + * no CDI / DI Framework to rely on. + *

      + * EDC {@link Inject} only works in {@link CrawlerExtension}. + */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class CrawlerExtensionContextBuilder { + + public static CrawlerExtensionContext buildContext( + Config config, + Monitor monitor, + TypeManager typeManager, + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd, + CatalogService catalogService + ) { + // Config + var crawlerConfigFactory = new CrawlerConfigFactory(config); + var crawlerConfig = crawlerConfigFactory.buildCrawlerConfig(); + + // DB + var dataSourceFactory = new DataSourceFactory(config); + var dataSource = dataSourceFactory.newDataSource(); + var flywayService = new FlywayService(config, monitor, dataSource); + flywayService.validateOrMigrateInTests(); + + // Dao + var dataOfferQueries = new DataOfferQueries(); + var dslContextFactory = new DslContextFactory(dataSource); + var connectorQueries = new ConnectorQueries(crawlerConfig); + + // Services + var objectMapperJsonLd = getJsonLdObjectMapper(typeManager); + var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd); + var policyMapper = newPolicyMapper(typeTransformerRegistry, objectMapperJsonLd); + var crawlerEventLogger = new CrawlerEventLogger(); + var crawlerExecutionTimeLogger = new CrawlerExecutionTimeLogger(); + var dataOfferMappingUtils = new FetchedCatalogMappingUtils( + policyMapper, + assetMapper, + objectMapperJsonLd + ); + var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); + var dataOfferRecordUpdater = new DataOfferRecordUpdater(connectorQueries); + var contractOfferQueries = new ContractOfferQueries(); + var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(crawlerConfig, crawlerEventLogger); + var dataOfferPatchBuilder = new CatalogPatchBuilder( + contractOfferQueries, + dataOfferQueries, + dataOfferRecordUpdater, + contractOfferRecordUpdater + ); + var dataOfferPatchApplier = new CatalogPatchApplier(); + var dataOfferWriter = new ConnectorUpdateCatalogWriter(dataOfferPatchBuilder, dataOfferPatchApplier); + var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter( + crawlerEventLogger, + dataOfferWriter, + dataOfferLimitsEnforcer + ); + var fetchedDataOfferBuilder = new FetchedCatalogBuilder(dataOfferMappingUtils); + var dspDataOfferBuilder = new DspDataOfferBuilder(jsonLd); + var dspCatalogService = new DspCatalogService( + catalogService, + dspDataOfferBuilder + ); + var dataOfferFetcher = new FetchedCatalogService(dspCatalogService, fetchedDataOfferBuilder); + var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(crawlerEventLogger, monitor); + var connectorUpdater = new ConnectorCrawler( + dataOfferFetcher, + connectorUpdateSuccessWriter, + connectorUpdateFailureWriter, + connectorQueries, + dslContextFactory, + monitor, + crawlerExecutionTimeLogger + ); + + var threadPoolTaskQueue = new ThreadPoolTaskQueue(); + var threadPool = new ThreadPool(threadPoolTaskQueue, crawlerConfig, monitor); + var connectorQueue = new ConnectorQueue(connectorUpdater, threadPool); + var connectorQueueFiller = new ConnectorQueueFiller(connectorQueue, connectorQueries); + var connectorStatusUpdater = new ConnectorStatusUpdater(); + var catalogCleaner = new CatalogCleaner(); + var offlineConnectorCleaner = new OfflineConnectorCleaner( + crawlerConfig, + connectorQueries, + crawlerEventLogger, + connectorStatusUpdater, + catalogCleaner + ); + + // Schedules + List> jobs = List.of( + getOnlineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getOfflineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getDeadConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getOfflineConnectorCleanerCronJob(dslContextFactory, offlineConnectorCleaner) + ); + + // Startup + var quartzScheduleInitializer = new QuartzScheduleInitializer(config, monitor, jobs); + var crawlerInitializer = new CrawlerInitializer(quartzScheduleInitializer); + + return new CrawlerExtensionContext( + crawlerInitializer, + dataSource, + dslContextFactory, + connectorUpdater, + policyMapper, + fetchedDataOfferBuilder, + dataOfferRecordUpdater + ); + } + + @NotNull + private static PolicyMapper newPolicyMapper(TypeTransformerRegistry typeTransformerRegistry, ObjectMapper objectMapperJsonLd) { + var operatorMapper = new OperatorMapper(); + var literalMapper = new LiteralMapper( + objectMapperJsonLd + ); + var atomicConstraintMapper = new AtomicConstraintMapper( + literalMapper, + operatorMapper + ); + var policyValidator = new PolicyValidator(); + var constraintExtractor = new ConstraintExtractor( + policyValidator, + atomicConstraintMapper + ); + return new PolicyMapper( + constraintExtractor, + atomicConstraintMapper, + typeTransformerRegistry + ); + } + + @NotNull + private static AssetMapper newAssetMapper( + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd + ) { + var edcPropertyUtils = new EdcPropertyUtils(); + var assetJsonLdUtils = new AssetJsonLdUtils(); + var assetEditRequestMapper = new AssetEditRequestMapper(); + var shortDescriptionBuilder = new ShortDescriptionBuilder(); + var assetJsonLdParser = new AssetJsonLdParser( + assetJsonLdUtils, + shortDescriptionBuilder, + endpoint -> false + ); + var httpHeaderMapper = new HttpHeaderMapper(); + var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); + var dataSourceMapper = new DataSourceMapper( + edcPropertyUtils, + httpDataSourceMapper + ); + var assetJsonLdBuilder = new AssetJsonLdBuilder( + dataSourceMapper, + assetJsonLdParser, + assetEditRequestMapper + ); + return new AssetMapper( + typeTransformerRegistry, + assetJsonLdBuilder, + assetJsonLdParser, + jsonLd + ); + } + + @NotNull + private static CronJobRef getOfflineConnectorCleanerCronJob(DslContextFactory dslContextFactory, + OfflineConnectorCleaner offlineConnectorCleaner) { + return new CronJobRef<>( + CrawlerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, + OfflineConnectorCleanerJob.class, + () -> new OfflineConnectorCleanerJob(dslContextFactory, offlineConnectorCleaner) + ); + } + + @NotNull + private static CronJobRef getOnlineConnectorRefreshCronJob( + DslContextFactory dslContextFactory, + ConnectorQueueFiller connectorQueueFiller + ) { + return new CronJobRef<>( + CrawlerExtension.CRON_ONLINE_CONNECTOR_REFRESH, + OnlineConnectorRefreshJob.class, + () -> new OnlineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ); + } + + @NotNull + private static CronJobRef getOfflineConnectorRefreshCronJob( + DslContextFactory dslContextFactory, + ConnectorQueueFiller connectorQueueFiller + ) { + return new CronJobRef<>( + CrawlerExtension.CRON_OFFLINE_CONNECTOR_REFRESH, + OfflineConnectorRefreshJob.class, + () -> new OfflineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ); + } + + @NotNull + private static CronJobRef getDeadConnectorRefreshCronJob(DslContextFactory dslContextFactory, + ConnectorQueueFiller connectorQueueFiller) { + return new CronJobRef<>( + CrawlerExtension.CRON_DEAD_CONNECTOR_REFRESH, + DeadConnectorRefreshJob.class, + () -> new DeadConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + ); + } + + private static ObjectMapper getJsonLdObjectMapper(TypeManager typeManager) { + var objectMapper = typeManager.getMapper(CoreConstants.JSON_LD); + + // Fixes Dates in JSON-LD Object Mapper + // The Core EDC uses longs over OffsetDateTime, so they never fixed the date format + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + return objectMapper; + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerInitializer.java similarity index 56% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerInitializer.java index 4f1ca59f2..719ace1c3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/BrokerServerInitializer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerInitializer.java @@ -12,20 +12,16 @@ * */ -package de.sovity.edc.ext.brokerserver.services; +package de.sovity.edc.ext.catalog.crawler; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.schedules.QuartzScheduleInitializer; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.QuartzScheduleInitializer; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public class BrokerServerInitializer { - private final DslContextFactory dslContextFactory; - private final KnownConnectorsInitializer knownConnectorsInitializer; +public class CrawlerInitializer { private final QuartzScheduleInitializer quartzScheduleInitializer; public void onStartup() { - dslContextFactory.transaction(knownConnectorsInitializer::addKnownConnectorsOnStartup); quartzScheduleInitializer.startSchedules(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/ConnectorCrawler.java similarity index 53% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/ConnectorCrawler.java index 5a759fab0..dca422308 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdater.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/ConnectorCrawler.java @@ -12,14 +12,16 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing; +package de.sovity.edc.ext.catalog.crawler.crawling; -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.MeasurementErrorStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerExecutionTimeLogger; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.CatalogFetcher; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.FetchedCatalogService; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerExecutionTimeLogger; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.ConnectorUpdateFailureWriter; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.ConnectorUpdateSuccessWriter; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.MeasurementErrorStatus; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.time.StopWatch; import org.eclipse.edc.spi.monitor.Monitor; @@ -30,41 +32,41 @@ * Updates a single connector. */ @RequiredArgsConstructor -public class ConnectorUpdater { - private final CatalogFetcher catalogFetcher; +public class ConnectorCrawler { + private final FetchedCatalogService fetchedCatalogService; private final ConnectorUpdateSuccessWriter connectorUpdateSuccessWriter; private final ConnectorUpdateFailureWriter connectorUpdateFailureWriter; private final ConnectorQueries connectorQueries; private final DslContextFactory dslContextFactory; private final Monitor monitor; - private final BrokerExecutionTimeLogger brokerExecutionTimeLogger; + private final CrawlerExecutionTimeLogger crawlerExecutionTimeLogger; /** * Updates single connector. * - * @param connectorEndpoint connector endpoint + * @param connectorRef connector */ - public void updateConnector(String connectorEndpoint) { + public void crawlConnector(ConnectorRef connectorRef) { var executionTime = StopWatch.createStarted(); var failed = false; try { - monitor.info("Updating connector: " + connectorEndpoint); + monitor.info("Updating connector: " + connectorRef); - var catalog = catalogFetcher.fetchCatalog(connectorEndpoint); + var catalog = fetchedCatalogService.fetchCatalog(connectorRef); // Update connector in a single transaction dslContextFactory.transaction(dsl -> { - ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); - connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRecord, catalog); + var connectorRecord = connectorQueries.findByConnectorId(dsl, connectorRef.getConnectorId()); + connectorUpdateSuccessWriter.handleConnectorOnline(dsl, connectorRef, connectorRecord, catalog); }); } catch (Exception e) { failed = true; try { // Update connector in a single transaction dslContextFactory.transaction(dsl -> { - ConnectorRecord connectorRecord = connectorQueries.findByEndpoint(dsl, connectorEndpoint); - connectorUpdateFailureWriter.handleConnectorOffline(dsl, connectorRecord, e); + var connectorRecord = connectorQueries.findByConnectorId(dsl, connectorRef.getConnectorId()); + connectorUpdateFailureWriter.handleConnectorOffline(dsl, connectorRef, connectorRecord, e); }); } catch (Exception e1) { e1.addSuppressed(e); @@ -74,9 +76,12 @@ public void updateConnector(String connectorEndpoint) { executionTime.stop(); try { var status = failed ? MeasurementErrorStatus.ERROR : MeasurementErrorStatus.OK; - dslContextFactory.transaction(dsl -> { - brokerExecutionTimeLogger.logExecutionTime(dsl, connectorEndpoint, executionTime.getTime(TimeUnit.MILLISECONDS), status); - }); + dslContextFactory.transaction(dsl -> crawlerExecutionTimeLogger.logExecutionTime( + dsl, + connectorRef, + executionTime.getTime(TimeUnit.MILLISECONDS), + status + )); } catch (Exception e) { monitor.severe("Failed logging connector update execution time.", e); } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/OfflineConnectorCleaner.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/OfflineConnectorCleaner.java new file mode 100644 index 000000000..51f5c9bd4 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/OfflineConnectorCleaner.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling; + +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.dao.CatalogCleaner; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorStatusUpdater; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class OfflineConnectorCleaner { + private final CrawlerConfig crawlerConfig; + private final ConnectorQueries connectorQueries; + private final CrawlerEventLogger crawlerEventLogger; + private final ConnectorStatusUpdater connectorStatusUpdater; + private final CatalogCleaner catalogCleaner; + + public void cleanConnectorsIfOfflineTooLong(DSLContext dsl) { + var killOfflineConnectorsAfter = crawlerConfig.getKillOfflineConnectorsAfter(); + var connectorsToKill = connectorQueries.findAllConnectorsForKilling(dsl, killOfflineConnectorsAfter); + + catalogCleaner.removeCatalogByConnectors(dsl, connectorsToKill); + connectorStatusUpdater.markAsDead(dsl, connectorsToKill); + + crawlerEventLogger.addKilledDueToOfflineTooLongMessages(dsl, connectorsToKill); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogBuilder.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogBuilder.java new file mode 100644 index 000000000..2ef73ffa2 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogBuilder.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.fetching; + +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedCatalog; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.utils.catalog.model.DspCatalog; +import de.sovity.edc.utils.catalog.model.DspContractOffer; +import de.sovity.edc.utils.catalog.model.DspDataOffer; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@RequiredArgsConstructor +public class FetchedCatalogBuilder { + private final FetchedCatalogMappingUtils fetchedCatalogMappingUtils; + + public FetchedCatalog buildFetchedCatalog(DspCatalog catalog, ConnectorRef connectorRef) { + assertEqualEndpoint(catalog, connectorRef); + assertEqualParticipantId(catalog, connectorRef); + + var fetchedDataOffers = catalog.getDataOffers().stream() + .map(dspDataOffer -> buildFetchedDataOffer(dspDataOffer, connectorRef)) + .toList(); + + var fetchedCatalog = new FetchedCatalog(); + fetchedCatalog.setConnectorRef(connectorRef); + fetchedCatalog.setDataOffers(fetchedDataOffers); + return fetchedCatalog; + } + + private void assertEqualParticipantId(DspCatalog catalog, ConnectorRef connectorRef) { + Validate.isTrue( + connectorRef.getConnectorId().equals(catalog.getParticipantId()), + String.format( + "Connector connectorId does not match the participantId: connectorId %s, participantId %s", + connectorRef.getConnectorId(), + catalog.getParticipantId() + ) + ); + } + + private void assertEqualEndpoint(DspCatalog catalog, ConnectorRef connectorRef) { + Validate.isTrue( + connectorRef.getEndpoint().equals(catalog.getEndpoint()), + String.format( + "Connector endpoint mismatch: expected %s, got %s", + connectorRef.getEndpoint(), + catalog.getEndpoint() + ) + ); + } + + @NotNull + private FetchedDataOffer buildFetchedDataOffer( + DspDataOffer dspDataOffer, + ConnectorRef connectorRef + ) { + var uiAsset = fetchedCatalogMappingUtils.buildUiAsset(dspDataOffer, connectorRef); + var uiAssetJson = fetchedCatalogMappingUtils.buildUiAssetJson(uiAsset); + + var fetchedDataOffer = new FetchedDataOffer(); + fetchedDataOffer.setAssetId(uiAsset.getAssetId()); + fetchedDataOffer.setUiAsset(uiAsset); + fetchedDataOffer.setUiAssetJson(uiAssetJson); + fetchedDataOffer.setContractOffers(buildFetchedContractOffers(dspDataOffer.getContractOffers())); + return fetchedDataOffer; + } + + @NotNull + private List buildFetchedContractOffers(List offers) { + return offers.stream() + .map(this::buildFetchedContractOffer) + .toList(); + } + + @NotNull + private FetchedContractOffer buildFetchedContractOffer(DspContractOffer offer) { + var uiPolicyJson = fetchedCatalogMappingUtils.buildUiPolicyJson(offer.getPolicyJsonLd()); + var contractOffer = new FetchedContractOffer(); + contractOffer.setContractOfferId(offer.getContractOfferId()); + contractOffer.setUiPolicyJson(uiPolicyJson); + return contractOffer; + } + +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogMappingUtils.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogMappingUtils.java new file mode 100644 index 000000000..1ed02ef3e --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogMappingUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.fetching; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; +import de.sovity.edc.utils.catalog.model.DspDataOffer; +import jakarta.json.JsonObject; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +public class FetchedCatalogMappingUtils { + private final PolicyMapper policyMapper; + private final AssetMapper assetMapper; + private final ObjectMapper objectMapper; + + public UiAsset buildUiAsset( + DspDataOffer dspDataOffer, + ConnectorRef connectorRef + ) { + var assetJsonLd = assetMapper.buildAssetJsonLdFromDatasetProperties(dspDataOffer.getAssetPropertiesJsonLd()); + var asset = assetMapper.buildAsset(assetJsonLd); + var uiAsset = assetMapper.buildUiAsset(asset, connectorRef.getEndpoint(), connectorRef.getConnectorId()); + uiAsset.setCreatorOrganizationName(connectorRef.getOrganizationLegalName()); + uiAsset.setParticipantId(connectorRef.getConnectorId()); + return uiAsset; + } + + @SneakyThrows + public String buildUiAssetJson(UiAsset uiAsset) { + return objectMapper.writeValueAsString(uiAsset); + } + + @SneakyThrows + public String buildUiPolicyJson(JsonObject policyJsonLd) { + var policy = policyMapper.buildPolicy(policyJsonLd); + var uiPolicy = policyMapper.buildUiPolicy(policy); + return objectMapper.writeValueAsString(uiPolicy); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogService.java similarity index 59% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogService.java index 2b7da851c..c55973bbe 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/CatalogFetcher.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/FetchedCatalogService.java @@ -12,29 +12,30 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.crawling.fetching; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedCatalog; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedCatalog; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; import de.sovity.edc.utils.catalog.DspCatalogService; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; @RequiredArgsConstructor -public class CatalogFetcher { +public class FetchedCatalogService { private final DspCatalogService dspCatalogService; - private final FetchedCatalogBuilder fetchedCatalogBuilder; + private final FetchedCatalogBuilder catalogPatchBuilder; /** * Fetches {@link ContractOffer}s and de-duplicates them into {@link FetchedDataOffer}s. * - * @param connectorEndpoint connector endpoint + * @param connectorRef connector * @return updated connector db row */ @SneakyThrows - public FetchedCatalog fetchCatalog(String connectorEndpoint) { - var dspCatalog = dspCatalogService.fetchDataOffers(connectorEndpoint); - return fetchedCatalogBuilder.buildFetchedCatalog(dspCatalog); + public FetchedCatalog fetchCatalog(ConnectorRef connectorRef) { + var dspCatalog = dspCatalogService.fetchDataOffers(connectorRef.getEndpoint()); + return catalogPatchBuilder.buildFetchedCatalog(dspCatalog, connectorRef); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java similarity index 80% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java index 550dd0657..d930eb223 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedCatalog.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedCatalog.java @@ -12,8 +12,9 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; +package de.sovity.edc.ext.catalog.crawler.crawling.fetching.model; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -28,6 +29,6 @@ @Setter @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedCatalog { - String participantId; + ConnectorRef connectorRef; List dataOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedContractOffer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java similarity index 86% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedContractOffer.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java index b2d566f70..64317498e 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedContractOffer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedContractOffer.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; +package de.sovity.edc.ext.catalog.crawler.crawling.fetching.model; import lombok.AccessLevel; import lombok.Getter; @@ -24,5 +24,5 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedContractOffer { String contractOfferId; - String policyJson; + String uiPolicyJson; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java similarity index 68% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java index d93306613..752fc98fd 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/model/FetchedDataOffer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/fetching/model/FetchedDataOffer.java @@ -12,8 +12,9 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers.model; +package de.sovity.edc.ext.catalog.crawler.crawling.fetching.model; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -29,15 +30,7 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class FetchedDataOffer { String assetId; - String assetTitle; - String description; - String curatorOrganizationName; - String dataCategory; - String dataSubcategory; - String dataModel; - String transportMode; - String geoReferenceMethod; - List keywords; - String assetJsonLd; + UiAsset uiAsset; + String uiAssetJson; List contractOffers; } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/ConnectorChangeTracker.java similarity index 96% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/ConnectorChangeTracker.java index 1513c1f5e..e87eabb01 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/ConnectorChangeTracker.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/ConnectorChangeTracker.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.logging; +package de.sovity.edc.ext.catalog.crawler.crawling.logging; import lombok.Getter; import lombok.Setter; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventErrorMessage.java similarity index 57% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventErrorMessage.java index b3746df1f..9d7ca039a 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/logging/BrokerEventErrorMessage.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventErrorMessage.java @@ -12,32 +12,32 @@ * */ -package de.sovity.edc.ext.brokerserver.services.logging; +package de.sovity.edc.ext.catalog.crawler.crawling.logging; -import de.sovity.edc.ext.brokerserver.utils.StringUtils2; +import de.sovity.edc.ext.catalog.crawler.utils.StringUtils2; import lombok.NonNull; import org.apache.commons.lang3.exception.ExceptionUtils; /** * Helper Dto that contains User Message + Error Stack Trace to be written into - * {@link de.sovity.edc.ext.brokerserver.db.jooq.tables.BrokerEventLog}. + * {@link de.sovity.edc.ext.catalog.crawler.db.jooq.tables.CrawlerEventLog}. *
      * This class exists so that logging exceptions has a consistent format. * - * @param message message + * @param message message * @param stackTraceOrNull stack trace */ -public record BrokerEventErrorMessage(String message, String stackTraceOrNull) { +public record CrawlerEventErrorMessage(String message, String stackTraceOrNull) { - public static BrokerEventErrorMessage ofMessage(@NonNull String message) { - return new BrokerEventErrorMessage(message, null); + public static CrawlerEventErrorMessage ofMessage(@NonNull String message) { + return new CrawlerEventErrorMessage(message, null); } - public static BrokerEventErrorMessage ofStackTrace(@NonNull String baseMessage, @NonNull Throwable cause) { + public static CrawlerEventErrorMessage ofStackTrace(@NonNull String baseMessage, @NonNull Throwable cause) { var message = baseMessage; message = StringUtils2.removeSuffix(message, "."); message = StringUtils2.removeSuffix(message, ":"); message = "%s: %s".formatted(message, cause.getClass().getName()); - return new BrokerEventErrorMessage(message, ExceptionUtils.getStackTrace(cause)); + return new CrawlerEventErrorMessage(message, ExceptionUtils.getStackTrace(cause)); } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLogger.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLogger.java new file mode 100644 index 000000000..396e2095e --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLogger.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.logging; + +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.CrawlerEventStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.CrawlerEventType; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.CrawlerEventLogRecord; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.UUID; + +/** + * Updates a single connector. + */ +@RequiredArgsConstructor +public class CrawlerEventLogger { + + public void logConnectorUpdated(DSLContext dsl, ConnectorRef connectorRef, ConnectorChangeTracker changes) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_UPDATED); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage(changes.toString()); + logEntry.insert(); + } + + public void logConnectorOffline(DSLContext dsl, ConnectorRef connectorRef, CrawlerEventErrorMessage errorMessage) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_STATUS_CHANGE_OFFLINE); + logEntry.setEventStatus(CrawlerEventStatus.ERROR); + logEntry.setUserMessage("Connector is offline."); + logEntry.setErrorStack(errorMessage.stackTraceOrNull()); + logEntry.insert(); + } + + public void logConnectorOnline(DSLContext dsl, ConnectorRef connectorRef) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_STATUS_CHANGE_ONLINE); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage("Connector is online."); + logEntry.insert(); + } + + public void logConnectorUpdateDataOfferLimitExceeded( + DSLContext dsl, + ConnectorRef connectorRef, + Integer maxDataOffersPerConnector + ) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_DATA_OFFER_LIMIT_EXCEEDED); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage( + "Connector has more than %d data offers. Exceeding data offers will be ignored.".formatted(maxDataOffersPerConnector)); + logEntry.insert(); + } + + public void logConnectorUpdateDataOfferLimitOk(DSLContext dsl, ConnectorRef connectorRef) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_DATA_OFFER_LIMIT_OK); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage("Connector is not exceeding the maximum number of data offers limit anymore."); + logEntry.insert(); + } + + public void logConnectorUpdateContractOfferLimitExceeded( + DSLContext dsl, + ConnectorRef connectorRef, + Integer maxContractOffersPerConnector + ) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_EXCEEDED); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage(String.format( + "Some data offers have more than %d contract offers. Exceeding contract offers will be ignored.: ", + maxContractOffersPerConnector + )); + logEntry.insert(); + } + + public void logConnectorUpdateContractOfferLimitOk(DSLContext dsl, ConnectorRef connectorRef) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_CONTRACT_OFFER_LIMIT_OK); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage("Connector is not exceeding the maximum number of contract offers per data offer limit anymore."); + logEntry.insert(); + } + + public void addKilledDueToOfflineTooLongMessages(DSLContext dsl, Collection connectorRefs) { + var logEntries = connectorRefs.stream() + .map(connectorRef -> buildKilledDueToOfflineTooLongMessage(dsl, connectorRef)) + .toList(); + dsl.batchInsert(logEntries).execute(); + } + + private CrawlerEventLogRecord buildKilledDueToOfflineTooLongMessage(DSLContext dsl, ConnectorRef connectorRef) { + var logEntry = newLogEntry(dsl, connectorRef); + logEntry.setEvent(CrawlerEventType.CONNECTOR_KILLED_DUE_TO_OFFLINE_FOR_TOO_LONG); + logEntry.setEventStatus(CrawlerEventStatus.OK); + logEntry.setUserMessage("Connector was marked as dead for being offline too long."); + return logEntry; + } + + private CrawlerEventLogRecord newLogEntry(DSLContext dsl, ConnectorRef connectorRef) { + var logEntry = dsl.newRecord(Tables.CRAWLER_EVENT_LOG); + logEntry.setId(UUID.randomUUID()); + logEntry.setEnvironment(connectorRef.getEnvironmentId()); + logEntry.setConnectorId(connectorRef.getConnectorId()); + logEntry.setCreatedAt(OffsetDateTime.now()); + return logEntry; + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerExecutionTimeLogger.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerExecutionTimeLogger.java new file mode 100644 index 000000000..b04e757f4 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerExecutionTimeLogger.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.logging; + +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.MeasurementErrorStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.MeasurementType; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.util.UUID; + +/** + * Updates a single connector. + */ +@RequiredArgsConstructor +public class CrawlerExecutionTimeLogger { + public void logExecutionTime(DSLContext dsl, ConnectorRef connectorRef, long executionTimeInMs, MeasurementErrorStatus errorStatus) { + var logEntry = dsl.newRecord(Tables.CRAWLER_EXECUTION_TIME_MEASUREMENT); + logEntry.setId(UUID.randomUUID()); + logEntry.setEnvironment(connectorRef.getEnvironmentId()); + logEntry.setConnectorId(connectorRef.getConnectorId()); + logEntry.setDurationInMs(executionTimeInMs); + logEntry.setType(MeasurementType.CONNECTOR_REFRESH); + logEntry.setErrorStatus(errorStatus); + logEntry.setCreatedAt(OffsetDateTime.now()); + logEntry.insert(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/CatalogPatchBuilder.java similarity index 67% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/CatalogPatchBuilder.java index 8b7dcc6be..309e8d8fc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferPatchBuilder.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/CatalogPatchBuilder.java @@ -12,15 +12,19 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.dao.ContractOfferQueries; -import de.sovity.edc.ext.brokerserver.dao.DataOfferQueries; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.DataOfferPatch; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.utils.DiffUtils; +import de.sovity.edc.ext.catalog.crawler.dao.CatalogPatch; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferQueries; +import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferRecordUpdater; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferQueries; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferRecordUpdater; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ContractOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -31,7 +35,7 @@ import static java.util.stream.Collectors.groupingBy; @RequiredArgsConstructor -public class DataOfferPatchBuilder { +public class CatalogPatchBuilder { private final ContractOfferQueries contractOfferQueries; private final DataOfferQueries dataOfferQueries; private final DataOfferRecordUpdater dataOfferRecordUpdater; @@ -40,19 +44,19 @@ public class DataOfferPatchBuilder { /** * Fetches existing data offers of given connector endpoint and compares them with fetched data offers. * - * @param dsl dsl - * @param connectorEndpoint connector endpoint + * @param dsl dsl + * @param connectorRef connector * @param fetchedDataOffers fetched data offers * @return change list / patch */ - public DataOfferPatch buildDataOfferPatch( + public CatalogPatch buildDataOfferPatch( DSLContext dsl, - String connectorEndpoint, + ConnectorRef connectorRef, Collection fetchedDataOffers ) { - var patch = new DataOfferPatch(); - var dataOffers = dataOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint); - var contractOffersByAssetId = contractOfferQueries.findByConnectorEndpoint(dsl, connectorEndpoint) + var patch = new CatalogPatch(); + var dataOffers = dataOfferQueries.findByConnectorId(dsl, connectorRef.getConnectorId()); + var contractOffersByAssetId = contractOfferQueries.findByConnectorId(dsl, connectorRef.getConnectorId()) .stream() .collect(groupingBy(ContractOfferRecord::getAssetId)); @@ -64,8 +68,8 @@ public DataOfferPatch buildDataOfferPatch( ); diff.added().forEach(fetched -> { - var newRecord = dataOfferRecordUpdater.newDataOffer(connectorEndpoint, fetched); - patch.insertDataOffer(newRecord); + var newRecord = dataOfferRecordUpdater.newDataOffer(connectorRef, fetched); + patch.dataOffers().insert(newRecord); patchContractOffers(patch, newRecord, List.of(), fetched.getContractOffers()); }); @@ -81,21 +85,21 @@ public DataOfferPatch buildDataOfferPatch( changed = dataOfferRecordUpdater.updateDataOffer(existing, fetched, changed); if (changed) { - patch.updateDataOffer(existing); + patch.dataOffers().update(existing); } }); diff.removed().forEach(dataOffer -> { - patch.deleteDataOffer(dataOffer); + patch.dataOffers().delete(dataOffer); var contractOffers = contractOffersByAssetId.getOrDefault(dataOffer.getAssetId(), List.of()); - contractOffers.forEach(patch::deleteContractOffer); + contractOffers.forEach(it -> patch.contractOffers().delete(it)); }); return patch; } private boolean patchContractOffers( - DataOfferPatch patch, + CatalogPatch patch, DataOfferRecord dataOffer, Collection contractOffers, Collection fetchedContractOffers @@ -111,7 +115,7 @@ private boolean patchContractOffers( diff.added().forEach(fetched -> { var newRecord = contractOfferRecordUpdater.newContractOffer(dataOffer, fetched); - patch.insertContractOffer(newRecord); + patch.contractOffers().insert(newRecord); hasUpdates.set(true); }); @@ -120,13 +124,13 @@ private boolean patchContractOffers( var fetched = match.fetched(); if (contractOfferRecordUpdater.updateContractOffer(existing, fetched)) { - patch.updateContractOffer(existing); + patch.contractOffers().update(existing); hasUpdates.set(true); } }); diff.removed().forEach(existing -> { - patch.deleteContractOffer(existing); + patch.contractOffers().delete(existing); hasUpdates.set(true); }); diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriter.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriter.java new file mode 100644 index 000000000..ec67dd2a2 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriter.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.ConnectorChangeTracker; +import de.sovity.edc.ext.catalog.crawler.dao.CatalogPatchApplier; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.jooq.DSLContext; + +import java.util.Collection; + +@RequiredArgsConstructor +public class ConnectorUpdateCatalogWriter { + private final CatalogPatchBuilder catalogPatchBuilder; + private final CatalogPatchApplier catalogPatchApplier; + + /** + * Updates a connector's data offers with given {@link FetchedDataOffer}s. + * + * @param dsl dsl + * @param connectorRef connector + * @param fetchedDataOffers fetched data offers + * @param changes change tracker for log message + */ + @SneakyThrows + public void updateDataOffers( + DSLContext dsl, + ConnectorRef connectorRef, + Collection fetchedDataOffers, + ConnectorChangeTracker changes + ) { + var patch = catalogPatchBuilder.buildDataOfferPatch(dsl, connectorRef, fetchedDataOffers); + changes.setNumOffersAdded(patch.dataOffers().getInsertions().size()); + changes.setNumOffersUpdated(patch.dataOffers().getUpdates().size()); + changes.setNumOffersDeleted(patch.dataOffers().getDeletions().size()); + catalogPatchApplier.applyDbUpdatesBatched(dsl, patch); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateFailureWriter.java similarity index 51% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateFailureWriter.java index 87dd61389..05b7f1bb1 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/ConnectorUpdateFailureWriter.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateFailureWriter.java @@ -12,12 +12,13 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventErrorMessage; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventErrorMessage; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.monitor.Monitor; import org.jooq.DSLContext; @@ -26,14 +27,19 @@ @RequiredArgsConstructor public class ConnectorUpdateFailureWriter { - private final BrokerEventLogger brokerEventLogger; + private final CrawlerEventLogger crawlerEventLogger; private final Monitor monitor; - public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Throwable e) { + public void handleConnectorOffline( + DSLContext dsl, + ConnectorRef connectorRef, + ConnectorRecord connector, + Throwable e + ) { // Log Status Change and set status to offline if necessary if (connector.getOnlineStatus() == ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { - monitor.info("Connector is offline: " + connector.getEndpoint(), e); - brokerEventLogger.logConnectorOffline(dsl, connector.getEndpoint(), getFailureMessage(e)); + monitor.info("Connector is offline: " + connector.getEndpointUrl(), e); + crawlerEventLogger.logConnectorOffline(dsl, connectorRef, getFailureMessage(e)); connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); } @@ -41,7 +47,7 @@ public void handleConnectorOffline(DSLContext dsl, ConnectorRecord connector, Th connector.update(); } - public BrokerEventErrorMessage getFailureMessage(Throwable e) { - return BrokerEventErrorMessage.ofStackTrace("Unexpected exception during connector update.", e); + public CrawlerEventErrorMessage getFailureMessage(Throwable e) { + return CrawlerEventErrorMessage.ofStackTrace("Unexpected exception during connector update.", e); } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateSuccessWriter.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateSuccessWriter.java new file mode 100644 index 000000000..32b5ebe5e --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateSuccessWriter.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedCatalog; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.ConnectorChangeTracker; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; + +@RequiredArgsConstructor +public class ConnectorUpdateSuccessWriter { + private final CrawlerEventLogger crawlerEventLogger; + private final ConnectorUpdateCatalogWriter connectorUpdateCatalogWriter; + private final DataOfferLimitsEnforcer dataOfferLimitsEnforcer; + + public void handleConnectorOnline( + DSLContext dsl, + ConnectorRef connectorRef, + ConnectorRecord connector, + FetchedCatalog catalog + ) { + // Limit data offers and log limitation if necessary + var limitedDataOffers = dataOfferLimitsEnforcer.enforceLimits(catalog.getDataOffers()); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connectorRef, connector, limitedDataOffers); + + // Log Status Change and set status to online if necessary + if (connector.getOnlineStatus() != ConnectorOnlineStatus.ONLINE || connector.getLastRefreshAttemptAt() == null) { + crawlerEventLogger.logConnectorOnline(dsl, connectorRef); + connector.setOnlineStatus(ConnectorOnlineStatus.ONLINE); + } + + // Track changes for final log message + var changes = new ConnectorChangeTracker(); + var now = OffsetDateTime.now(); + connector.setLastSuccessfulRefreshAt(now); + connector.setLastRefreshAttemptAt(now); + connector.update(); + + // Update data offers + connectorUpdateCatalogWriter.updateDataOffers( + dsl, + connectorRef, + limitedDataOffers.abbreviatedDataOffers(), + changes + ); + + // Log event if changes are present + if (!changes.isEmpty()) { + crawlerEventLogger.logConnectorUpdated(dsl, connectorRef, changes); + } + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferLimitsEnforcer.java similarity index 62% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferLimitsEnforcer.java index b4d067881..0583b93b3 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferLimitsEnforcer.java @@ -12,14 +12,15 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -29,8 +30,8 @@ @RequiredArgsConstructor public class DataOfferLimitsEnforcer { - private final BrokerServerSettings brokerServerSettings; - private final BrokerEventLogger brokerEventLogger; + private final CrawlerConfig crawlerConfig; + private final CrawlerEventLogger crawlerEventLogger; public record DataOfferLimitsEnforced( Collection abbreviatedDataOffers, @@ -41,8 +42,8 @@ public record DataOfferLimitsEnforced( public DataOfferLimitsEnforced enforceLimits(Collection dataOffers) { // Get limits from config - var maxDataOffers = brokerServerSettings.getMaxDataOffersPerConnector(); - var maxContractOffers = brokerServerSettings.getMaxContractOffersPerDataOffer(); + var maxDataOffers = crawlerConfig.getMaxDataOffersPerConnector(); + var maxContractOffers = crawlerConfig.getMaxContractOffersPerDataOffer(); List offerList = new ArrayList<>(dataOffers); // No limits set @@ -70,26 +71,31 @@ public DataOfferLimitsEnforced enforceLimits(Collection dataOf return new DataOfferLimitsEnforced(offerList, dataOfferLimitsExceeded, contractOfferLimitsExceeded); } - public void logEnforcedLimitsIfChanged(DSLContext dsl, ConnectorRecord connector, DataOfferLimitsEnforced enforcedLimits) { - String endpoint = connector.getEndpoint(); + public void logEnforcedLimitsIfChanged( + DSLContext dsl, + ConnectorRef connectorRef, + ConnectorRecord connector, + DataOfferLimitsEnforced enforcedLimits + ) { // DataOffer if (enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.OK) { - var maxDataOffers = brokerServerSettings.getMaxDataOffersPerConnector(); - brokerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, maxDataOffers, endpoint); + var maxDataOffers = crawlerConfig.getMaxDataOffersPerConnector(); + crawlerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, connectorRef, maxDataOffers); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.EXCEEDED); } else if (!enforcedLimits.dataOfferLimitsExceeded() && connector.getDataOffersExceeded() == ConnectorDataOffersExceeded.EXCEEDED) { - brokerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, endpoint); + crawlerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, connectorRef); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); } // ContractOffer if (enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.OK) { - var maxContractOffers = brokerServerSettings.getMaxContractOffersPerDataOffer(); - brokerEventLogger.logConnectorUpdateContractOfferLimitExceeded(dsl, maxContractOffers, endpoint); + var maxContractOffers = crawlerConfig.getMaxContractOffersPerDataOffer(); + crawlerEventLogger.logConnectorUpdateContractOfferLimitExceeded(dsl, connectorRef, maxContractOffers); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.EXCEEDED); - } else if (!enforcedLimits.contractOfferLimitsExceeded() && connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.EXCEEDED) { - brokerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, endpoint); + } else if (!enforcedLimits.contractOfferLimitsExceeded() && + connector.getContractOffersExceeded() == ConnectorContractOffersExceeded.EXCEEDED) { + crawlerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, connectorRef); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/ChangeTracker.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/ChangeTracker.java new file mode 100644 index 000000000..dad7f9cae --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/ChangeTracker.java @@ -0,0 +1,36 @@ +package de.sovity.edc.ext.catalog.crawler.crawling.writing.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class ChangeTracker { + private boolean changed = false; + + public void setIfChanged( + T existing, + T fetched, + Consumer setter + ) { + setIfChanged(existing, fetched, setter, Objects::equals); + } + + public void setIfChanged( + T existing, + T fetched, + Consumer setter, + BiPredicate equalityChecker + ) { + if (!equalityChecker.test(existing, fetched)) { + setter.accept(fetched); + changed = true; + } + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/DiffUtils.java similarity index 72% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/DiffUtils.java index 413ba77a2..7db2dac4f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtils.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/utils/DiffUtils.java @@ -12,9 +12,9 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.crawling.writing.utils; -import de.sovity.edc.ext.brokerserver.utils.MapUtils; +import de.sovity.edc.ext.catalog.crawler.utils.MapUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -30,13 +30,13 @@ public class DiffUtils { /** * Tries to match two collections by a key, then collects planned change sets as {@link DiffResult}. * - * @param existing list of existing elements + * @param existing list of existing elements * @param existingKeyFn existing elements key extractor - * @param fetched list of fetched elements - * @param fetchedKeyFn fetched elements key extractor - * @param first collection type - * @param second collection type - * @param key type + * @param fetched list of fetched elements + * @param fetchedKeyFn fetched elements key extractor + * @param first collection type + * @param second collection type + * @param key type */ public static DiffResult compareLists( Collection existing, @@ -71,13 +71,13 @@ public static DiffResult compareLists( /** * Result of comparing two collections by keys. * - * @param added elements that are present in fetched collection but not in existing + * @param added elements that are present in fetched collection but not in existing * @param updated elements that are present in both collections * @param removed elements that are present in existing collection but not in fetched - * @param existing item type - * @param fetched item type + * @param existing item type + * @param fetched item type */ - record DiffResult(List added, List> updated, List removed) { + public record DiffResult(List added, List> updated, List removed) { DiffResult() { this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } @@ -87,10 +87,10 @@ record DiffResult(List added, List> updated, List * Pair of elements that are present in both collections. * * @param existing existing item - * @param fetched fetched item - * @param existing item type - * @param fetched item type + * @param fetched fetched item + * @param existing item type + * @param fetched item type */ - record DiffResultMatch(A existing, B fetched) { + public record DiffResultMatch(A existing, B fetched) { } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogCleaner.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogCleaner.java new file mode 100644 index 000000000..87b17d337 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogCleaner.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao; + +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; + +import java.util.Collection; + +import static java.util.stream.Collectors.toSet; + +@RequiredArgsConstructor +public class CatalogCleaner { + + public void removeCatalogByConnectors(DSLContext dsl, Collection connectorRefs) { + var co = Tables.CONTRACT_OFFER; + var d = Tables.DATA_OFFER; + + var connectorIds = connectorRefs.stream().map(ConnectorRef::getConnectorId).collect(toSet()); + + dsl.deleteFrom(co).where(PostgresqlUtils.in(co.CONNECTOR_ID, connectorIds)).execute(); + dsl.deleteFrom(d).where(PostgresqlUtils.in(d.CONNECTOR_ID, connectorIds)).execute(); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatch.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatch.java new file mode 100644 index 000000000..8ad4ebc1e --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatch.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao; + +import de.sovity.edc.ext.catalog.crawler.dao.utils.RecordPatch; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ContractOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +/** + * Contains planned DB Row changes to be applied as batch. + */ +@Getter +@Setter +@Accessors(fluent = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CatalogPatch { + RecordPatch dataOffers = new RecordPatch<>(); + RecordPatch contractOffers = new RecordPatch<>(); + + public List> insertionOrder() { + return List.of(dataOffers, contractOffers); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatchApplier.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatchApplier.java new file mode 100644 index 000000000..9dce2fc9d --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/CatalogPatchApplier.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao; + +import de.sovity.edc.ext.catalog.crawler.dao.utils.RecordPatch; +import de.sovity.edc.ext.catalog.crawler.utils.CollectionUtils2; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class CatalogPatchApplier { + + @SneakyThrows + public void applyDbUpdatesBatched(DSLContext dsl, CatalogPatch catalogPatch) { + var insertionOrder = catalogPatch.insertionOrder(); + var deletionOrder = CollectionUtils2.reverse(insertionOrder); + + insertionOrder.stream() + .map(RecordPatch::getInsertions) + .filter(CollectionUtils2::isNotEmpty) + .forEach(it -> dsl.batchInsert(it).execute()); + + insertionOrder.stream() + .map(RecordPatch::getUpdates) + .filter(CollectionUtils2::isNotEmpty) + .forEach(it -> dsl.batchUpdate(it).execute()); + + deletionOrder.stream() + .map(RecordPatch::getDeletions) + .filter(CollectionUtils2::isNotEmpty) + .forEach(it -> dsl.batchDelete(it).execute()); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DataSourceFactory.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DataSourceFactory.java new file mode 100644 index 000000000..b66997470 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DataSourceFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.config; + +import com.zaxxer.hikari.HikariDataSource; +import de.sovity.edc.ext.catalog.crawler.CrawlerExtension; +import de.sovity.edc.extension.postgresql.HikariDataSourceFactory; +import de.sovity.edc.extension.postgresql.JdbcCredentials; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.Validate; +import org.eclipse.edc.spi.system.configuration.Config; + +import javax.sql.DataSource; + +@RequiredArgsConstructor +public class DataSourceFactory { + private final Config config; + + + /** + * Create a new {@link DataSource} from EDC Config. + * + * @return {@link DataSource}. + */ + public HikariDataSource newDataSource() { + var jdbcCredentials = getJdbcCredentials(); + int maxPoolSize = config.getInteger(CrawlerExtension.DB_CONNECTION_POOL_SIZE); + int connectionTimeoutInMs = config.getInteger(CrawlerExtension.DB_CONNECTION_TIMEOUT_IN_MS); + return HikariDataSourceFactory.newDataSource( + jdbcCredentials, + maxPoolSize, + connectionTimeoutInMs + ); + } + + + public JdbcCredentials getJdbcCredentials() { + return new JdbcCredentials( + getRequiredStringProperty(config, CrawlerExtension.JDBC_URL), + getRequiredStringProperty(config, CrawlerExtension.JDBC_USER), + getRequiredStringProperty(config, CrawlerExtension.JDBC_PASSWORD) + ); + } + + private String getRequiredStringProperty(Config config, String name) { + String value = config.getString(name, ""); + Validate.notBlank(value, "EDC Property '%s' is required".formatted(name)); + return value; + } + +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DslContextFactory.java similarity index 67% rename from extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DslContextFactory.java index 4aafd44fe..6f97871bb 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DslContextFactory.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/DslContextFactory.java @@ -1,11 +1,10 @@ -package de.sovity.edc.ext.brokerserver.db; +package de.sovity.edc.ext.catalog.crawler.dao.config; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.impl.DSL; -import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import javax.sql.DataSource; @@ -23,8 +22,7 @@ public class DslContextFactory { * @return new {@link DSLContext} */ public DSLContext newDslContext() { - var globalDslContextForDbTests = DslContextFactoryHijacker.getParentDslContext(); - return Objects.requireNonNullElseGet(globalDslContextForDbTests, () -> DSL.using(dataSource, SQLDialect.POSTGRES)); + return DSL.using(dataSource, SQLDialect.POSTGRES); } /** @@ -47,4 +45,23 @@ public R transactionResult(Function function) { public void transaction(Consumer function) { newDslContext().transaction(transaction -> function.accept(transaction.dsl())); } + + /** + * Runs given code within a test transaction. + * + * @param code code to run within the test transaction + */ + public void testTransaction(Consumer code) { + try { + transaction(dsl -> { + code.accept(dsl); + throw new TestTransactionNoopException(); + }); + } catch (TestTransactionNoopException e) { + // Ignore + } + } + + private static class TestTransactionNoopException extends RuntimeException { + } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/FlywayService.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/FlywayService.java new file mode 100644 index 000000000..dee88e32b --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/config/FlywayService.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.config; + +import de.sovity.edc.ext.catalog.crawler.CrawlerExtension; +import de.sovity.edc.extension.postgresql.FlywayExecutionParams; +import de.sovity.edc.extension.postgresql.FlywayUtils; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; + +import javax.sql.DataSource; + +@RequiredArgsConstructor +public class FlywayService { + private final Config config; + private final Monitor monitor; + private final DataSource dataSource; + + public void validateOrMigrateInTests() { + var additionalLocations = config.getString(CrawlerExtension.DB_ADDITIONAL_FLYWAY_MIGRATION_LOCATIONS, ""); + + var params = baseConfig(additionalLocations) + .clean(config.getBoolean(CrawlerExtension.DB_CLEAN, false)) + .cleanEnabled(config.getBoolean(CrawlerExtension.DB_CLEAN_ENABLED, false)) + .migrate(config.getBoolean(CrawlerExtension.DB_MIGRATE, false)) + .infoLogger(monitor::info) + .build(); + + FlywayUtils.cleanAndMigrate(params, dataSource); + } + + public static FlywayExecutionParams.FlywayExecutionParamsBuilder baseConfig(String additionalMigrationLocations) { + var migrationLocations = FlywayUtils.parseFlywayLocations( + "classpath:db-crawler/migration,%s".formatted(additionalMigrationLocations) + ); + + return FlywayExecutionParams.builder() + .migrationLocations(migrationLocations) + .table("flyway_schema_history"); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java new file mode 100644 index 000000000..4c3e3843b --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.connectors; + +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.Connector; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.Organization; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jooq.Condition; +import org.jooq.DSLContext; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiFunction; + +@RequiredArgsConstructor +public class ConnectorQueries { + private final CrawlerConfig crawlerConfig; + + public ConnectorRecord findByConnectorId(DSLContext dsl, String connectorId) { + var c = Tables.CONNECTOR; + return dsl.fetchOne(c, c.CONNECTOR_ID.eq(connectorId)); + } + + public Set findConnectorsForScheduledRefresh(DSLContext dsl, ConnectorOnlineStatus onlineStatus) { + return queryConnectorRefs(dsl, (c, o) -> c.ONLINE_STATUS.eq(onlineStatus)); + } + + public Set findAllConnectorsForKilling(DSLContext dsl, Duration deleteOfflineConnectorsAfter) { + var minLastRefresh = OffsetDateTime.now().minus(deleteOfflineConnectorsAfter); + return queryConnectorRefs(dsl, (c, o) -> c.LAST_SUCCESSFUL_REFRESH_AT.lt(minLastRefresh)); + } + + @NotNull + private Set queryConnectorRefs( + DSLContext dsl, + BiFunction condition + ) { + var c = Tables.CONNECTOR; + var o = Tables.ORGANIZATION; + var query = dsl.select( + c.CONNECTOR_ID.as("connectorId"), + c.ENVIRONMENT.as("environmentId"), + o.NAME.as("organizationLegalName"), + o.MDS_ID.as("organizationId"), + c.ENDPOINT_URL.as("endpoint") + ) + .from(c) + .leftJoin(o).on(c.MDS_ID.eq(o.MDS_ID)) + .where(condition.apply(c, o), c.ENVIRONMENT.eq(crawlerConfig.getEnvironmentId())) + .fetchInto(ConnectorRef.class); + + return new HashSet<>(query); + } +} diff --git a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRef.java similarity index 51% rename from extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRef.java index 38e404bb5..3d94fca72 100644 --- a/extensions/broker-server-api/api/src/main/java/de/sovity/edc/ext/brokerserver/api/model/CatalogPageSortingType.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRef.java @@ -12,21 +12,21 @@ * */ -package de.sovity.edc.ext.brokerserver.api.model; +package de.sovity.edc.ext.catalog.crawler.dao.connectors; -import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.ToString; @Getter @RequiredArgsConstructor -@Schema(description = "Catalog's sorting options") -public enum CatalogPageSortingType { - VIEW_COUNT("By Popularity"), - MOST_RECENT("Most Recent"), - TITLE("By Title"), - ORIGINATOR("By Connector"); - - private final String title; +@EqualsAndHashCode(of = "connectorId", callSuper = false) +@ToString(of = "connectorId") +public class ConnectorRef { + private final String connectorId; + private final String environmentId; + private final String organizationLegalName; + private final String organizationId; + private final String endpoint; } - diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorStatusUpdater.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorStatusUpdater.java new file mode 100644 index 000000000..6dfad8125 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorStatusUpdater.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.connectors; + +import de.sovity.edc.ext.catalog.crawler.dao.utils.PostgresqlUtils; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import org.jooq.DSLContext; + +import java.util.Collection; +import java.util.stream.Collectors; + +public class ConnectorStatusUpdater { + public void markAsDead(DSLContext dsl, Collection connectorRefs) { + var connectorIds = connectorRefs.stream() + .map(ConnectorRef::getConnectorId) + .collect(Collectors.toSet()); + var c = Tables.CONNECTOR; + dsl.update(c).set(c.ONLINE_STATUS, ConnectorOnlineStatus.DEAD) + .where(PostgresqlUtils.in(c.CONNECTOR_ID, connectorIds)).execute(); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ContractOfferQueries.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers/ContractOfferQueries.java similarity index 56% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ContractOfferQueries.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers/ContractOfferQueries.java index be5ef734c..0d350e429 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/ContractOfferQueries.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers/ContractOfferQueries.java @@ -12,18 +12,18 @@ * */ -package de.sovity.edc.ext.brokerserver.dao; +package de.sovity.edc.ext.catalog.crawler.dao.contract_offers; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ContractOfferRecord; import org.jooq.DSLContext; import java.util.List; public class ContractOfferQueries { - public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { + public List findByConnectorId(DSLContext dsl, String connectorId) { var co = Tables.CONTRACT_OFFER; - return dsl.selectFrom(co).where(co.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); + return dsl.selectFrom(co).where(co.CONNECTOR_ID.eq(connectorId)).stream().toList(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers/ContractOfferRecordUpdater.java similarity index 52% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers/ContractOfferRecordUpdater.java index 3b3980294..ed23d2f07 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/ContractOfferRecordUpdater.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/contract_offers/ContractOfferRecordUpdater.java @@ -12,17 +12,18 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.dao.contract_offers; -import de.sovity.edc.ext.brokerserver.dao.utils.JsonbUtils; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.utils.ChangeTracker; +import de.sovity.edc.ext.catalog.crawler.dao.utils.JsonbUtils; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ContractOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.catalog.crawler.utils.JsonUtils2; import lombok.RequiredArgsConstructor; import org.jooq.JSONB; import java.time.OffsetDateTime; -import java.util.Objects; /** * Creates or updates {@link ContractOfferRecord} DB Rows. @@ -35,13 +36,17 @@ public class ContractOfferRecordUpdater { /** * Create new {@link ContractOfferRecord} from {@link FetchedContractOffer}. * - * @param dataOffer parent data offer db row + * @param dataOffer parent data offer db row * @param fetchedContractOffer fetched contract offer * @return new db row */ - public ContractOfferRecord newContractOffer(DataOfferRecord dataOffer, FetchedContractOffer fetchedContractOffer) { + public ContractOfferRecord newContractOffer( + DataOfferRecord dataOffer, + FetchedContractOffer fetchedContractOffer + ) { var contractOffer = new ContractOfferRecord(); - contractOffer.setConnectorEndpoint(dataOffer.getConnectorEndpoint()); + + contractOffer.setConnectorId(dataOffer.getConnectorId()); contractOffer.setContractOfferId(fetchedContractOffer.getContractOfferId()); contractOffer.setAssetId(dataOffer.getAssetId()); contractOffer.setCreatedAt(OffsetDateTime.now()); @@ -52,24 +57,27 @@ public ContractOfferRecord newContractOffer(DataOfferRecord dataOffer, FetchedCo /** * Update existing {@link ContractOfferRecord} with changes from {@link FetchedContractOffer}. * - * @param contractOffer existing row + * @param contractOffer existing row * @param fetchedContractOffer changes to be integrated * @return if anything was changed */ - public boolean updateContractOffer(ContractOfferRecord contractOffer, FetchedContractOffer fetchedContractOffer) { - var existingPolicy = JsonbUtils.getDataOrNull(contractOffer.getPolicy()); - var fetchedPolicy = fetchedContractOffer.getPolicyJson(); - var changed = false; + public boolean updateContractOffer( + ContractOfferRecord contractOffer, + FetchedContractOffer fetchedContractOffer + ) { + var changes = new ChangeTracker(); - if (!Objects.equals(existingPolicy, fetchedPolicy)) { - contractOffer.setPolicy(JSONB.jsonb(fetchedPolicy)); - changed = true; - } + changes.setIfChanged( + JsonbUtils.getDataOrNull(contractOffer.getUiPolicyJson()), + fetchedContractOffer.getUiPolicyJson(), + it -> contractOffer.setUiPolicyJson(JSONB.jsonb(it)), + JsonUtils2::isEqualJson + ); - if (changed) { + if (changes.isChanged()) { contractOffer.setUpdatedAt(OffsetDateTime.now()); } - return changed; + return changes.isChanged(); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferQueries.java similarity index 59% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferQueries.java index fed263bd0..f4a82b14c 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/DataOfferQueries.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferQueries.java @@ -12,10 +12,10 @@ * */ -package de.sovity.edc.ext.brokerserver.dao; +package de.sovity.edc.ext.catalog.crawler.dao.data_offers; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -24,8 +24,8 @@ @RequiredArgsConstructor public class DataOfferQueries { - public List findByConnectorEndpoint(DSLContext dsl, String connectorEndpoint) { + public List findByConnectorId(DSLContext dsl, String connectorId) { var d = Tables.DATA_OFFER; - return dsl.selectFrom(d).where(d.CONNECTOR_ENDPOINT.eq(connectorEndpoint)).stream().toList(); + return dsl.selectFrom(d).where(d.CONNECTOR_ID.eq(connectorId)).stream().toList(); } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java new file mode 100644 index 000000000..955c05271 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/data_offers/DataOfferRecordUpdater.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.data_offers; + +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.utils.ChangeTracker; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.dao.utils.JsonbUtils; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.catalog.crawler.utils.JsonUtils2; +import lombok.RequiredArgsConstructor; +import org.jooq.JSONB; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Creates or updates {@link DataOfferRecord} DB Rows. + *

      + * (Or at least prepares them for batch inserts / updates) + */ +@RequiredArgsConstructor +public class DataOfferRecordUpdater { + + private final ConnectorQueries connectorQueries; + + /** + * Create a new {@link DataOfferRecord}. + * + * @param connectorRef connector + * @param fetchedDataOffer new db row data + * @return new db row + */ + public DataOfferRecord newDataOffer( + ConnectorRef connectorRef, + FetchedDataOffer fetchedDataOffer + ) { + var dataOffer = new DataOfferRecord(); + var connectorId = connectorRef.getConnectorId(); + + dataOffer.setConnectorId(connectorId); + dataOffer.setAssetId(fetchedDataOffer.getAssetId()); + dataOffer.setCreatedAt(OffsetDateTime.now()); + updateDataOffer(dataOffer, fetchedDataOffer, true); + return dataOffer; + } + + + /** + * Update existing {@link DataOfferRecord}. + * + * @param record existing row + * @param fetchedDataOffer changes to be incorporated + * @param changed whether the data offer should be marked as updated simply because the contract offers changed + * @return whether any fields were updated + */ + public boolean updateDataOffer( + DataOfferRecord record, + FetchedDataOffer fetchedDataOffer, + boolean changed + ) { + var asset = fetchedDataOffer.getUiAsset(); + var changes = new ChangeTracker(changed); + + changes.setIfChanged( + blankIfNull(record.getAssetTitle()), + blankIfNull(asset.getTitle()), + record::setAssetTitle + ); + + changes.setIfChanged( + blankIfNull(record.getDescription()), + blankIfNull(asset.getDescription()), + record::setDescription + ); + + changes.setIfChanged( + blankIfNull(record.getDataCategory()), + blankIfNull(asset.getDataCategory()), + record::setDataCategory + ); + + changes.setIfChanged( + blankIfNull(record.getDataSubcategory()), + blankIfNull(asset.getDataSubcategory()), + record::setDataSubcategory + ); + + changes.setIfChanged( + blankIfNull(record.getDataModel()), + blankIfNull(asset.getDataModel()), + record::setDataModel + ); + + changes.setIfChanged( + blankIfNull(record.getTransportMode()), + blankIfNull(asset.getTransportMode()), + record::setTransportMode + ); + + changes.setIfChanged( + blankIfNull(record.getGeoReferenceMethod()), + blankIfNull(asset.getGeoReferenceMethod()), + record::setGeoReferenceMethod + ); + + changes.setIfChanged( + emptyIfNull(record.getKeywords()), + emptyIfNull(asset.getKeywords()), + it -> { + record.setKeywords(it.toArray(new String[0])); + record.setKeywordsCommaJoined(String.join(", ", it)); + } + ); + + changes.setIfChanged( + JsonbUtils.getDataOrNull(record.getUiAssetJson()), + fetchedDataOffer.getUiAssetJson(), + it -> record.setUiAssetJson(JSONB.jsonb(it)), + JsonUtils2::isEqualJson + ); + + if (changes.isChanged()) { + record.setUpdatedAt(OffsetDateTime.now()); + } + + return changes.isChanged(); + } + + private String blankIfNull(String string) { + return string == null ? "" : string; + } + + private Collection emptyIfNull(Collection collection) { + return collection == null ? List.of() : collection; + } + + private Collection emptyIfNull(T[] array) { + return array == null ? List.of() : Arrays.asList(array); + } + +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonbUtils.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/JsonbUtils.java similarity index 94% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonbUtils.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/JsonbUtils.java index 0a5157624..c3afdbabd 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/JsonbUtils.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/JsonbUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.utils; +package de.sovity.edc.ext.catalog.crawler.dao.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/PostgresqlUtils.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/PostgresqlUtils.java similarity index 92% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/PostgresqlUtils.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/PostgresqlUtils.java index e5291de5e..4c7cd3e41 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/dao/utils/PostgresqlUtils.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/PostgresqlUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.dao.utils; +package de.sovity.edc.ext.catalog.crawler.dao.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -31,7 +31,7 @@ public class PostgresqlUtils { /** * Replaces the IN operation with "field = ANY(...)" * - * @param field field + * @param field field * @param values values * @return condition */ diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/RecordPatch.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/RecordPatch.java new file mode 100644 index 000000000..99c6a025a --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/utils/RecordPatch.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.utils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import org.jooq.UpdatableRecord; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains planned DB Row changes to be applied as batch. + */ +@Getter +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class RecordPatch> { + List insertions = new ArrayList<>(); + List updates = new ArrayList<>(); + List deletions = new ArrayList<>(); + + public void insert(T record) { + insertions.add(record); + } + + public void update(T record) { + updates.add(record); + } + + public void delete(T record) { + deletions.add(record); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfig.java similarity index 72% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfig.java index cc4083105..af3e2a0f4 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/BrokerServerSettings.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfig.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.config; +package de.sovity.edc.ext.catalog.crawler.orchestration.config; import lombok.Builder; import lombok.Value; @@ -21,14 +21,8 @@ @Value @Builder -public class BrokerServerSettings { - String adminApiKey; - Duration hideOfflineDataOffersAfter; - - int catalogPagePageSize; - - DataSpaceConfig dataSpaceConfig; - +public class CrawlerConfig { + String environmentId; int numThreads; Duration killOfflineConnectorsAfter; diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfigFactory.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfigFactory.java new file mode 100644 index 000000000..f40e6b6af --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/CrawlerConfigFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.orchestration.config; + +import de.sovity.edc.ext.catalog.crawler.CrawlerExtension; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.system.configuration.Config; + +import java.time.Duration; + +@RequiredArgsConstructor +public class CrawlerConfigFactory { + private final Config config; + + public CrawlerConfig buildCrawlerConfig() { + var environmentId = config.getString(CrawlerExtension.ENVIRONMENT_ID); + var numThreads = config.getInteger(CrawlerExtension.NUM_THREADS, 1); + var killOfflineConnectorsAfter = getDuration(CrawlerExtension.KILL_OFFLINE_CONNECTORS_AFTER, Duration.ofDays(5)); + var maxDataOffers = config.getInteger(CrawlerExtension.MAX_DATA_OFFERS_PER_CONNECTOR, -1); + var maxContractOffers = config.getInteger(CrawlerExtension.MAX_CONTRACT_OFFERS_PER_DATA_OFFER, -1); + + return CrawlerConfig.builder() + .environmentId(environmentId) + .numThreads(numThreads) + .killOfflineConnectorsAfter(killOfflineConnectorsAfter) + .maxDataOffersPerConnector(maxDataOffers) + .maxContractOffersPerDataOffer(maxContractOffers) + .build(); + } + + private Duration getDuration(@NonNull String configProperty, Duration defaultValue) { + var value = config.getString(configProperty, ""); + + if (StringUtils.isBlank(value)) { + return defaultValue; + } + + return Duration.parse(value); + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/EdcConfigPropertyUtils.java similarity index 83% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/EdcConfigPropertyUtils.java index 4ce3fd64b..c0d9bdb5f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/config/EdcConfigPropertyUtils.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/config/EdcConfigPropertyUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.config; +package de.sovity.edc.ext.catalog.crawler.orchestration.config; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -27,8 +27,8 @@ public class EdcConfigPropertyUtils { * For better refactoring it is better if the string constant is * found in the code as it is used and documented. * - * @param envVarName e.g. "BROKER_SERVER_SOME_CONFIG_SETTING" - * @return e.g. "broker.server.some.config.setting" + * @param envVarName e.g. "MY_EDC_PROP" + * @return e.g. "my.edc.prop" */ public static String toEdcProp(String envVarName) { return Arrays.stream(envVarName.split("_")) diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueue.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueue.java new file mode 100644 index 000000000..e46808a3a --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueue.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; + +import de.sovity.edc.ext.catalog.crawler.crawling.ConnectorCrawler; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Collection; + +@RequiredArgsConstructor +public class ConnectorQueue { + private final ConnectorCrawler connectorCrawler; + private final ThreadPool threadPool; + + /** + * Enqueues connectors for update. + * + * @param connectorRefs connectors + * @param priority priority from {@link ConnectorRefreshPriority} + */ + public void addAll(Collection connectorRefs, int priority) { + var queued = threadPool.getQueuedConnectorRefs(); + connectorRefs = new ArrayList<>(connectorRefs); + connectorRefs.removeIf(queued::contains); + + for (var connectorRef : connectorRefs) { + threadPool.enqueueConnectorRefreshTask( + priority, + () -> connectorCrawler.crawlConnector(connectorRef), + connectorRef + ); + } + } +} diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueueFiller.java similarity index 65% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueueFiller.java index e85a652e4..aaf2d7da9 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorQueueFiller.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorQueueFiller.java @@ -12,10 +12,10 @@ * */ -package de.sovity.edc.ext.brokerserver.services.queue; +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; @@ -25,7 +25,7 @@ public class ConnectorQueueFiller { private final ConnectorQueries connectorQueries; public void enqueueConnectors(DSLContext dsl, ConnectorOnlineStatus status, int priority) { - var endpoints = connectorQueries.findConnectorsForScheduledRefresh(dsl, status); - connectorQueue.addAll(endpoints, priority); + var connectorRefs = connectorQueries.findConnectorsForScheduledRefresh(dsl, status); + connectorQueue.addAll(connectorRefs, priority); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorRefreshPriority.java similarity index 76% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorRefreshPriority.java index 6931aad63..541c8528b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ConnectorRefreshPriority.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ConnectorRefreshPriority.java @@ -12,15 +12,12 @@ * */ -package de.sovity.edc.ext.brokerserver.services.queue; +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; import lombok.NoArgsConstructor; @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class ConnectorRefreshPriority { - public static final int ADMIN_REQUESTED = 1; - public static final int ADDED_ON_STARTUP = 10; - public static final int ADDED_ON_API_CALL = 50; public static final int SCHEDULED_ONLINE_REFRESH = 100; public static final int SCHEDULED_OFFLINE_REFRESH = 200; public static final int SCHEDULED_DEAD_REFRESH = 300; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPool.java similarity index 74% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPool.java index 022751243..857bf32ed 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPool.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPool.java @@ -12,9 +12,10 @@ * */ -package de.sovity.edc.ext.brokerserver.services.queue; +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; import org.eclipse.edc.spi.monitor.Monitor; import java.util.Set; @@ -27,9 +28,9 @@ public class ThreadPool { private final boolean enabled; private final ThreadPoolExecutor threadPoolExecutor; - public ThreadPool(ThreadPoolTaskQueue queue, BrokerServerSettings brokerServerSettings, Monitor monitor) { + public ThreadPool(ThreadPoolTaskQueue queue, CrawlerConfig crawlerConfig, Monitor monitor) { this.queue = queue; - int numThreads = brokerServerSettings.getNumThreads(); + int numThreads = crawlerConfig.getNumThreads(); enabled = numThreads > 0; if (enabled) { @@ -48,12 +49,12 @@ public ThreadPool(ThreadPoolTaskQueue queue, BrokerServerSettings brokerServerSe } } - public void enqueueConnectorRefreshTask(int priority, Runnable runnable, String endpoint) { - enqueueTask(new ThreadPoolTask(priority, runnable, endpoint)); + public void enqueueConnectorRefreshTask(int priority, Runnable runnable, ConnectorRef connectorRef) { + enqueueTask(new ThreadPoolTask(priority, runnable, connectorRef)); } - public Set getQueuedConnectorEndpoints() { - return queue.getConnectorEndpoints(); + public Set getQueuedConnectorRefs() { + return queue.getConnectorRefs(); } private void enqueueTask(ThreadPoolTask task) { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolTask.java similarity index 86% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolTask.java index ee69dd44f..66712b2d7 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTask.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolTask.java @@ -12,8 +12,9 @@ * */ -package de.sovity.edc.ext.brokerserver.services.queue; +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -35,7 +36,7 @@ public class ThreadPoolTask implements Runnable { private final long sequence = SEQ.incrementAndGet(); private final int priority; private final Runnable task; - private final String connectorEndpoint; + private final ConnectorRef connectorRef; @Override public void run() { diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolTaskQueue.java similarity index 84% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolTaskQueue.java index 44123180d..dc2b5598b 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/queue/ThreadPoolTaskQueue.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolTaskQueue.java @@ -12,8 +12,9 @@ * */ -package de.sovity.edc.ext.brokerserver.services.queue; +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -38,11 +39,11 @@ public void add(ThreadPoolTask task) { queue.add(task); } - public Set getConnectorEndpoints() { + public Set getConnectorRefs() { var queuedRunnables = new ArrayList<>(queue); return queuedRunnables.stream() - .map(ThreadPoolTask::getConnectorEndpoint) + .map(ThreadPoolTask::getConnectorRef) .filter(Objects::nonNull) .collect(Collectors.toSet()); } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/DeadConnectorRefreshJob.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/DeadConnectorRefreshJob.java similarity index 70% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/DeadConnectorRefreshJob.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/DeadConnectorRefreshJob.java index 63caca686..4f229b7fc 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/DeadConnectorRefreshJob.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/DeadConnectorRefreshJob.java @@ -12,12 +12,12 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorRefreshPriority; import lombok.RequiredArgsConstructor; import org.quartz.Job; import org.quartz.JobExecutionContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorCleanerJob.java similarity index 58% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorCleanerJob.java index f79826482..2c2e71853 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorKillerJob.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorCleanerJob.java @@ -12,21 +12,21 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.services.OfflineConnectorKiller; +import de.sovity.edc.ext.catalog.crawler.crawling.OfflineConnectorCleaner; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; import lombok.RequiredArgsConstructor; import org.quartz.Job; import org.quartz.JobExecutionContext; @RequiredArgsConstructor -public class OfflineConnectorKillerJob implements Job { +public class OfflineConnectorCleanerJob implements Job { private final DslContextFactory dslContextFactory; - private final OfflineConnectorKiller offlineConnectorKiller; + private final OfflineConnectorCleaner offlineConnectorCleaner; @Override public void execute(JobExecutionContext context) { - dslContextFactory.transaction(offlineConnectorKiller::killIfOfflineTooLong); + dslContextFactory.transaction(offlineConnectorCleaner::cleanConnectorsIfOfflineTooLong); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRefreshJob.java similarity index 70% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRefreshJob.java index 5cbeca52d..18965edf6 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OfflineConnectorRefreshJob.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRefreshJob.java @@ -12,12 +12,12 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorRefreshPriority; import lombok.RequiredArgsConstructor; import org.quartz.Job; import org.quartz.JobExecutionContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OnlineConnectorRefreshJob.java similarity index 70% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OnlineConnectorRefreshJob.java index 839a62321..6cad1899f 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/OnlineConnectorRefreshJob.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OnlineConnectorRefreshJob.java @@ -12,12 +12,12 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules; -import de.sovity.edc.ext.brokerserver.db.DslContextFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorOnlineStatus; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorQueueFiller; -import de.sovity.edc.ext.brokerserver.services.queue.ConnectorRefreshPriority; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorQueueFiller; +import de.sovity.edc.ext.catalog.crawler.orchestration.queue.ConnectorRefreshPriority; import lombok.RequiredArgsConstructor; import org.quartz.Job; import org.quartz.JobExecutionContext; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/QuartzScheduleInitializer.java similarity index 90% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/QuartzScheduleInitializer.java index 3e4fb294a..d4f4597e7 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/QuartzScheduleInitializer.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/QuartzScheduleInitializer.java @@ -12,10 +12,10 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules; -import de.sovity.edc.ext.brokerserver.services.schedules.utils.CronJobRef; -import de.sovity.edc.ext.brokerserver.services.schedules.utils.JobFactoryImpl; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.utils.CronJobRef; +import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.utils.JobFactoryImpl; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/utils/CronJobRef.java similarity index 75% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/utils/CronJobRef.java index a98ad2cd7..8f435fd23 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/CronJobRef.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/utils/CronJobRef.java @@ -12,19 +12,19 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules.utils; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules.utils; import org.quartz.Job; import java.util.function.Supplier; /** - * Broker Server CRON Job. + * CRON Job. * * @param configPropertyName EDC Config property that decides cron expression - * @param clazz class of the job - * @param factory factory that initializes the task class - * @param job type + * @param clazz class of the job + * @param factory factory that initializes the task class + * @param job type */ public record CronJobRef( String configPropertyName, diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/utils/JobFactoryImpl.java similarity index 95% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/utils/JobFactoryImpl.java index 9e1597ae2..003f0161d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/services/schedules/utils/JobFactoryImpl.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/utils/JobFactoryImpl.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.schedules.utils; +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules.utils; import lombok.NonNull; import org.quartz.Job; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/CollectionUtils2.java similarity index 82% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/CollectionUtils2.java index ef3337a62..f113d8601 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/CollectionUtils2.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -29,8 +29,8 @@ public class CollectionUtils2 { /** * Set Difference * - * @param a base set - * @param b remove these items + * @param a base set + * @param b remove these items * @param set item type * @return a difference b */ @@ -44,9 +44,9 @@ public static boolean isNotEmpty(Collection collection) { return collection != null && !collection.isEmpty(); } - public static List allElementsExceptForIndex(Collection source, int skipIndex) { + public static List reverse(List source) { var result = new ArrayList<>(source); - result.remove(skipIndex); + java.util.Collections.reverse(result); return result; } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/JsonUtils2.java similarity index 74% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/JsonUtils2.java index fbe1af366..a1d2d57bb 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/JsonUtils2.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; @@ -25,8 +25,8 @@ public class JsonUtils2 { @SneakyThrows public static boolean isEqualJson(String json, String otherJson) { - return - (json == null && otherJson == null) || - (json != null && otherJson != null && OBJECT_MAPPER.readTree(json).equals(OBJECT_MAPPER.readTree(otherJson))); + return (json == null && otherJson == null) || + (json != null && otherJson != null && + OBJECT_MAPPER.readTree(json).equals(OBJECT_MAPPER.readTree(otherJson))); } } diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/MapUtils.java similarity index 95% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/MapUtils.java index f43b31e56..8c2cd3a1d 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/MapUtils.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/MapUtils.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/StringUtils2.java similarity index 60% rename from extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java rename to extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/StringUtils2.java index cdf454183..190f16ede 100644 --- a/extensions/broker-server/src/main/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/utils/StringUtils2.java @@ -12,15 +12,11 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.NonNull; -import org.apache.commons.lang3.StringUtils; - -import java.util.List; -import java.util.stream.Stream; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class StringUtils2 { @@ -38,21 +34,4 @@ public static String removeSuffix(@NonNull String string, @NonNull String suffix } return string; } - - /** - * Splits a string into its words and returns them in lowercase. - * - * @param string string - * @return list of lowercase words - */ - public static List lowercaseWords(String string) { - if (StringUtils.isBlank(string)) { - return List.of(); - } - - return Stream.of(string.split("\\s+")) - .map(String::toLowerCase) - .filter(StringUtils::isNotBlank) - .toList(); - } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/catalog-crawler/catalog-crawler/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..5a369119d --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.ext.catalog.crawler.CrawlerExtension diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/AssertionUtils.java similarity index 83% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/AssertionUtils.java index c810d36eb..aef8012b7 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/AssertionUtils.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/AssertionUtils.java @@ -12,9 +12,8 @@ * */ -package de.sovity.edc.ext.brokerserver; +package de.sovity.edc.ext.catalog.crawler; -import de.sovity.edc.ext.brokerserver.client.gen.JSON; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.SneakyThrows; @@ -29,6 +28,6 @@ public static void assertEqualJson(String expected, String actual) { } public static void assertEqualUsingJson(Object expected, Object actual) { - assertEqualJson(JSON.serialize(expected), JSON.serialize(actual)); + assertEqualJson(JsonTestUtils.serialize(expected), JsonTestUtils.serialize(actual)); } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java new file mode 100644 index 000000000..7e7026048 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerTestDb.java @@ -0,0 +1,63 @@ +package de.sovity.edc.ext.catalog.crawler; + +import com.zaxxer.hikari.HikariDataSource; +import de.sovity.edc.ext.catalog.crawler.dao.config.DslContextFactory; +import de.sovity.edc.ext.catalog.crawler.dao.config.FlywayService; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.postgresql.FlywayUtils; +import de.sovity.edc.extension.postgresql.HikariDataSourceFactory; +import de.sovity.edc.extension.postgresql.JdbcCredentials; +import org.jooq.DSLContext; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.util.function.Consumer; + +public class CrawlerTestDb implements BeforeAllCallback, AfterAllCallback { + private final TestDatabaseViaTestcontainers db = new TestDatabaseViaTestcontainers(); + + + private HikariDataSource dataSource = null; + private DslContextFactory dslContextFactory = null; + + public void testTransaction(Consumer code) { + dslContextFactory.testTransaction(code); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + // Init DB + db.beforeAll(extensionContext); + + // Init Data Source + var credentials = new JdbcCredentials( + db.getJdbcCredentials().jdbcUrl(), + db.getJdbcCredentials().jdbcUser(), + db.getJdbcCredentials().jdbcPassword() + ); + dataSource = HikariDataSourceFactory.newDataSource(credentials, 10, 1000); + dslContextFactory = new DslContextFactory(dataSource); + + // Migrate DB + var params = FlywayService.baseConfig("classpath:db-crawler/migration-test-utils") + .migrate(true) + .build(); + try { + FlywayUtils.cleanAndMigrate(params, dataSource); + } catch (Exception e) { + var paramsWithClean = params.withClean(true).withCleanEnabled(true); + FlywayUtils.cleanAndMigrate(paramsWithClean, dataSource); + } + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + if (dataSource != null) { + dataSource.close(); + } + + // Close DB + db.afterAll(extensionContext); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/JsonTestUtils.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/JsonTestUtils.java new file mode 100644 index 000000000..ec4f26b5e --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/JsonTestUtils.java @@ -0,0 +1,23 @@ +package de.sovity.edc.ext.catalog.crawler; + +import lombok.SneakyThrows; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonTestUtils { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @SneakyThrows + public static String serialize(Object obj) { + return OBJECT_MAPPER.writeValueAsString(obj); + } + + @SneakyThrows + public static T deserialize(String json, Class clazz) { + return OBJECT_MAPPER.readValue(json, clazz); + } + + public static T jsonCast(Object obj, Class clazz) { + return deserialize(serialize(obj), clazz); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java new file mode 100644 index 000000000..b522c8881 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler; + +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; +import lombok.experimental.UtilityClass; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.util.function.Consumer; + +@UtilityClass +public class TestData { + public static OffsetDateTime old = OffsetDateTime.now().withNano(0).withSecond(0).withMinute(0).withHour(0).minusDays(100); + + public static ConnectorRef connectorRef = new ConnectorRef( + "MDSL1234XX.C1234XX", + "test", + "My Org", + "MDSL1234XX", + "https://example.com/api/dsp" + ); + + public static void insertConnector( + DSLContext dsl, + ConnectorRef connectorRef, + Consumer applier + ) { + var organization = dsl.newRecord(Tables.ORGANIZATION); + organization.setMdsId(connectorRef.getOrganizationId()); + organization.setName(connectorRef.getOrganizationLegalName()); + organization.insert(); + + var connector = dsl.newRecord(Tables.CONNECTOR); + connector.setEnvironment(connectorRef.getEnvironmentId()); + connector.setMdsId(connectorRef.getOrganizationId()); + connector.setConnectorId(connectorRef.getConnectorId()); + connector.setName(connectorRef.getConnectorId() + " Name"); + connector.setEndpointUrl(connectorRef.getEndpoint()); + connector.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + connector.setLastRefreshAttemptAt(null); + connector.setLastSuccessfulRefreshAt(null); + connector.setCreatedAt(old); + connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); + connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); + applier.accept(connector); + connector.insert(); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLoggerTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLoggerTest.java new file mode 100644 index 000000000..f48ba78c8 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/logging/CrawlerEventLoggerTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.logging; + +import de.sovity.edc.ext.catalog.crawler.CrawlerTestDb; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import org.jooq.DSLContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +class CrawlerEventLoggerTest { + @RegisterExtension + private static final CrawlerTestDb TEST_DATABASE = new CrawlerTestDb(); + + @Test + void testDataOfferWriter_allSortsOfUpdates() { + TEST_DATABASE.testTransaction(dsl -> { + var crawlerEventLogger = new CrawlerEventLogger(); + + // Test that insertions insert required fields and don't cause DB errors + var connectorRef = new ConnectorRef( + "MDSL1234XX.C1234XX", + "test", + "My Org", + "MDSL1234XX", + "https://example.com/api/dsp" + ); + crawlerEventLogger.logConnectorUpdated(dsl, connectorRef, new ConnectorChangeTracker()); + crawlerEventLogger.logConnectorOnline(dsl, connectorRef); + crawlerEventLogger.logConnectorOffline(dsl, connectorRef, new CrawlerEventErrorMessage("Message", "Stacktrace")); + crawlerEventLogger.logConnectorUpdateContractOfferLimitExceeded(dsl, connectorRef, 10); + crawlerEventLogger.logConnectorUpdateContractOfferLimitOk(dsl, connectorRef); + crawlerEventLogger.logConnectorUpdateDataOfferLimitExceeded(dsl, connectorRef, 10); + crawlerEventLogger.logConnectorUpdateDataOfferLimitOk(dsl, connectorRef); + + assertThat(numLogEntries(dsl)).isEqualTo(7); + }); + } + + private Integer numLogEntries(DSLContext dsl) { + return dsl.selectCount().from(Tables.CRAWLER_EVENT_LOG).fetchOne().component1(); + } +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriterTest.java similarity index 80% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriterTest.java index c8792e345..249d36764 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTest.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/ConnectorUpdateCatalogWriterTest.java @@ -12,17 +12,13 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.db.FlywayTestUtils; -import de.sovity.edc.ext.brokerserver.db.TestDatabase; -import de.sovity.edc.ext.brokerserver.db.TestDatabaseFactory; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.logging.ConnectorChangeTracker; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Do; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.AssertionUtils; +import de.sovity.edc.ext.catalog.crawler.CrawlerTestDb; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.ConnectorChangeTracker; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; import org.assertj.core.data.TemporalUnitLessThanOffset; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -30,18 +26,14 @@ import java.time.temporal.ChronoUnit; import java.util.List; -import static de.sovity.edc.ext.brokerserver.AssertionUtils.assertEqualJson; +import static de.sovity.edc.ext.catalog.crawler.crawling.writing.DataOfferWriterTestDataModels.Co; +import static de.sovity.edc.ext.catalog.crawler.crawling.writing.DataOfferWriterTestDataModels.Do; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; -class DataOfferWriterTest { - +class ConnectorUpdateCatalogWriterTest { @RegisterExtension - private static final TestDatabase TEST_DATABASE = TestDatabaseFactory.getTestDatabase(); - - @BeforeAll - static void setup() { - FlywayTestUtils.migrate(TEST_DATABASE); - } + private static final CrawlerTestDb TEST_DATABASE = new CrawlerTestDb(); @Test void testDataOfferWriter_allSortsOfUpdates() { @@ -49,7 +41,8 @@ void testDataOfferWriter_allSortsOfUpdates() { var testDydi = new DataOfferWriterTestDydi(); var testData = new DataOfferWriterTestDataHelper(); var changes = new ConnectorChangeTracker(); - var dataOfferWriter = testDydi.getDataOfferWriter(); + var dataOfferWriter = testDydi.getConnectorUpdateCatalogWriter(); + when(testDydi.getCrawlerConfig().getEnvironmentId()).thenReturn("test"); // arrange var unchanged = Do.forName("unchanged"); @@ -79,7 +72,8 @@ void testDataOfferWriter_allSortsOfUpdates() { testData.existing(addedCoExisting); testData.fetched(addedCoFetched); - var removedCoExisting = Do.forName("contractOfferRemoved").withContractOffer(new Co("removed co", "removed co")); + var removedCoExisting = Do.forName("contractOfferRemoved") + .withContractOffer(new Co("removed co", "removed co")); var removedCoFetched = Do.forName("contractOfferRemoved"); testData.existing(removedCoExisting); testData.fetched(removedCoFetched); @@ -88,7 +82,7 @@ void testDataOfferWriter_allSortsOfUpdates() { dsl.transaction(it -> testData.initialize(it.dsl())); dsl.transaction(it -> dataOfferWriter.updateDataOffers( it.dsl(), - testData.connectorEndpoint, + testData.connectorRef, testData.fetchedDataOffers, changes )); @@ -139,10 +133,11 @@ void testDataOfferWriter_allSortsOfUpdates() { }); } - private void assertAssetPropertiesEqual(DataOfferWriterTestDataHelper testData, DataOfferRecord actual, Do expected) { - var actualAssetJson = actual.getAssetJsonLd().data(); - var expectedAssetJson = testData.dummyAssetJson(expected); - assertEqualJson(actualAssetJson, expectedAssetJson); + private void assertAssetPropertiesEqual(DataOfferWriterTestDataHelper testData, DataOfferRecord actual, + Do expected) { + var actualUiAssetJson = actual.getUiAssetJson().data(); + var expectedUiAssetJson = testData.dummyAssetJson(expected); + AssertionUtils.assertEqualJson(actualUiAssetJson, expectedUiAssetJson); } private void assertPolicyEquals( @@ -152,8 +147,8 @@ private void assertPolicyEquals( Co expectedCo ) { var actualContractOffer = actual.getContractOffer(expectedDo.getAssetId(), expectedCo.getId()); - var actualPolicy = actualContractOffer.getPolicy().data(); - var expectedPolicy = scenario.dummyPolicyJson(expectedCo.getPolicyValue()); - assertThat(actualPolicy).isEqualTo(expectedPolicy); + var actualUiPolicyJson = actualContractOffer.getUiPolicyJson().data(); + var expectedUiPolicyJson = scenario.dummyPolicyJson(expectedCo.getPolicyValue()); + assertThat(actualUiPolicyJson).isEqualTo(expectedUiPolicyJson); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferLimitsEnforcerTest.java similarity index 81% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferLimitsEnforcerTest.java index 9a0da5f0c..cb0bafdb5 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferLimitsEnforcerTest.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferLimitsEnforcerTest.java @@ -12,15 +12,17 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorContractOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.enums.ConnectorDataOffersExceeded; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ConnectorRecord; -import de.sovity.edc.ext.brokerserver.services.config.BrokerServerSettings; -import de.sovity.edc.ext.brokerserver.services.logging.BrokerEventLogger; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorContractOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorDataOffersExceeded; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ConnectorRecord; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; +import org.assertj.core.api.Assertions; import org.jooq.DSLContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,15 +39,17 @@ class DataOfferLimitsEnforcerTest { DataOfferLimitsEnforcer dataOfferLimitsEnforcer; - BrokerServerSettings settings; - BrokerEventLogger brokerEventLogger; + CrawlerConfig settings; + CrawlerEventLogger crawlerEventLogger; DSLContext dsl; + ConnectorRef connectorRef = new DataOfferWriterTestDataHelper().connectorRef; + @BeforeEach void setup() { - settings = mock(BrokerServerSettings.class); - brokerEventLogger = mock(BrokerEventLogger.class); - dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(settings, brokerEventLogger); + settings = mock(CrawlerConfig.class); + crawlerEventLogger = mock(CrawlerEventLogger.class); + dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(settings, crawlerEventLogger); dsl = mock(DSLContext.class); } @@ -68,7 +72,7 @@ void no_limit_and_two_dataofffers_and_contractoffer_should_not_limit() { var dataOffersLimitExceeded = enforcedLimits.dataOfferLimitsExceeded(); // assert - assertThat(actual).hasSize(2); + Assertions.assertThat(actual).hasSize(2); assertFalse(contractOffersLimitExceeded); assertFalse(dataOffersLimitExceeded); } @@ -115,7 +119,7 @@ void limit_one_and_two_dataoffers_should_result_to_one() { // assert assertThat(actual).hasSize(1); - assertThat(actual.get(0).getContractOffers()).hasSize(1); + Assertions.assertThat(actual.get(0).getContractOffers()).hasSize(1); assertTrue(contractOffersLimitExceeded); assertTrue(dataOffersLimitExceeded); } @@ -124,7 +128,6 @@ void limit_one_and_two_dataoffers_should_result_to_one() { void verify_logConnectorUpdateDataOfferLimitExceeded() { // arrange var connector = new ConnectorRecord(); - connector.setEndpoint("http://localhost:8080"); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.OK); int maxDataOffers = 1; @@ -138,17 +141,16 @@ void verify_logConnectorUpdateDataOfferLimitExceeded() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connectorRef, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateDataOfferLimitExceeded(dsl, 1, connector.getEndpoint()); + verify(crawlerEventLogger).logConnectorUpdateDataOfferLimitExceeded(dsl, connectorRef, 1); } @Test void verify_logConnectorUpdateDataOfferLimitOk() { // arrange var connector = new ConnectorRecord(); - connector.setEndpoint("http://localhost:8080"); connector.setDataOffersExceeded(ConnectorDataOffersExceeded.EXCEEDED); int maxDataOffers = -1; @@ -162,17 +164,16 @@ void verify_logConnectorUpdateDataOfferLimitOk() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connectorRef, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateDataOfferLimitOk(dsl, connector.getEndpoint()); + verify(crawlerEventLogger).logConnectorUpdateDataOfferLimitOk(dsl, connectorRef); } @Test void verify_logConnectorUpdateContractOfferLimitExceeded() { // arrange var connector = new ConnectorRecord(); - connector.setEndpoint("http://localhost:8080"); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.OK); int maxDataOffers = 1; @@ -186,17 +187,16 @@ void verify_logConnectorUpdateContractOfferLimitExceeded() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connectorRef, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateContractOfferLimitExceeded(dsl, 1, connector.getEndpoint()); + verify(crawlerEventLogger).logConnectorUpdateContractOfferLimitExceeded(dsl, connectorRef, 1); } @Test void verify_logConnectorUpdateContractOfferLimitOk() { // arrange var connector = new ConnectorRecord(); - connector.setEndpoint("http://localhost:8080"); connector.setContractOffersExceeded(ConnectorContractOffersExceeded.EXCEEDED); int maxDataOffers = -1; @@ -210,9 +210,9 @@ void verify_logConnectorUpdateContractOfferLimitOk() { // act var enforcedLimits = dataOfferLimitsEnforcer.enforceLimits(dataOffers); - dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connector, enforcedLimits); + dataOfferLimitsEnforcer.logEnforcedLimitsIfChanged(dsl, connectorRef, connector, enforcedLimits); // assert - verify(brokerEventLogger).logConnectorUpdateContractOfferLimitOk(dsl, connector.getEndpoint()); + verify(crawlerEventLogger).logConnectorUpdateContractOfferLimitOk(dsl, connectorRef); } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDataHelper.java similarity index 53% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDataHelper.java index 664ac3b30..997141ab5 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataHelper.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDataHelper.java @@ -12,16 +12,17 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; - -import de.sovity.edc.ext.brokerserver.dao.ConnectorQueries; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; -import de.sovity.edc.ext.brokerserver.services.ConnectorCreator; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedContractOffer; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.model.FetchedDataOffer; -import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.TestData; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedContractOffer; +import de.sovity.edc.ext.catalog.crawler.crawling.fetching.model.FetchedDataOffer; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ContractOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.utils.JsonUtils; +import jakarta.json.Json; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -33,13 +34,9 @@ import java.util.Optional; import java.util.stream.Collectors; -import static de.sovity.edc.ext.brokerserver.TestAsset.getAssetJsonLd; -import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Co; -import static de.sovity.edc.ext.brokerserver.services.refreshing.offers.DataOfferWriterTestDataModels.Do; - class DataOfferWriterTestDataHelper { - String connectorEndpoint = "https://example.com/api/dsp"; - OffsetDateTime old = OffsetDateTime.now().withNano(0).withSecond(0).withMinute(0).withHour(0).minusDays(100); + OffsetDateTime old = TestData.old; + ConnectorRef connectorRef = TestData.connectorRef; List existingContractOffers = new ArrayList<>(); List existingDataOffers = new ArrayList<>(); List fetchedDataOffers = new ArrayList<>(); @@ -50,7 +47,7 @@ class DataOfferWriterTestDataHelper { * * @param dataOffer fetched data offer */ - public void fetched(Do dataOffer) { + public void fetched(DataOfferWriterTestDataModels.Do dataOffer) { Validate.notEmpty(dataOffer.getContractOffers()); fetchedDataOffers.add(dummyFetchedDataOffer(dataOffer)); } @@ -61,51 +58,58 @@ public void fetched(Do dataOffer) { * * @param dataOffer data offer */ - public void existing(Do dataOffer) { + public void existing(DataOfferWriterTestDataModels.Do dataOffer) { Validate.notEmpty(dataOffer.getContractOffers()); existingDataOffers.add(dummyDataOffer(dataOffer)); dataOffer.getContractOffers().stream() - .map(contractOffer -> dummyContractOffer(dataOffer, contractOffer)) - .forEach(existingContractOffers::add); + .map(contractOffer -> dummyContractOffer(dataOffer, contractOffer)) + .forEach(existingContractOffers::add); } public void initialize(DSLContext dsl) { - var connectorQueries = new ConnectorQueries(); - var connectorCreator = new ConnectorCreator(connectorQueries); - connectorCreator.addConnector(dsl, connectorEndpoint); + TestData.insertConnector(dsl, connectorRef, record -> { + }); dsl.batchInsert(existingDataOffers).execute(); dsl.batchInsert(existingContractOffers).execute(); } - private ContractOfferRecord dummyContractOffer(Do dataOffer, Co contractOffer) { + private ContractOfferRecord dummyContractOffer( + DataOfferWriterTestDataModels.Do dataOffer, + DataOfferWriterTestDataModels.Co contractOffer + ) { var contractOfferRecord = new ContractOfferRecord(); - contractOfferRecord.setConnectorEndpoint(connectorEndpoint); + contractOfferRecord.setConnectorId(connectorRef.getConnectorId()); contractOfferRecord.setAssetId(dataOffer.getAssetId()); contractOfferRecord.setContractOfferId(contractOffer.getId()); - contractOfferRecord.setPolicy(JSONB.valueOf(dummyPolicyJson(contractOffer.getPolicyValue()))); + contractOfferRecord.setUiPolicyJson(JSONB.valueOf(dummyPolicyJson(contractOffer.getPolicyValue()))); contractOfferRecord.setCreatedAt(old); contractOfferRecord.setUpdatedAt(old); return contractOfferRecord; } - private DataOfferRecord dummyDataOffer(Do dataOffer) { + private DataOfferRecord dummyDataOffer(DataOfferWriterTestDataModels.Do dataOffer) { var assetName = Optional.of(dataOffer.getAssetTitle()).orElse(dataOffer.getAssetId()); var dataOfferRecord = new DataOfferRecord(); - dataOfferRecord.setConnectorEndpoint(connectorEndpoint); + dataOfferRecord.setConnectorId(connectorRef.getConnectorId()); dataOfferRecord.setAssetId(dataOffer.getAssetId()); dataOfferRecord.setAssetTitle(assetName); - dataOfferRecord.setAssetJsonLd(JSONB.valueOf(dummyAssetJson(dataOffer))); + dataOfferRecord.setUiAssetJson(JSONB.valueOf(dummyAssetJson(dataOffer))); dataOfferRecord.setCreatedAt(old); dataOfferRecord.setUpdatedAt(old); return dataOfferRecord; } - private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { + private FetchedDataOffer dummyFetchedDataOffer(DataOfferWriterTestDataModels.Do dataOffer) { var fetchedDataOffer = new FetchedDataOffer(); fetchedDataOffer.setAssetId(dataOffer.getAssetId()); - fetchedDataOffer.setAssetTitle(dataOffer.getAssetTitle()); - fetchedDataOffer.setAssetJsonLd(dummyAssetJson(dataOffer)); + fetchedDataOffer.setUiAsset( + UiAsset.builder() + .assetId(dataOffer.getAssetId()) + .title(dataOffer.getAssetTitle()) + .build() + ); + fetchedDataOffer.setUiAssetJson(dummyAssetJson(dataOffer)); var contractOffersMapped = dataOffer.getContractOffers().stream().map(this::dummyFetchedContractOffer).collect(Collectors.toList()); fetchedDataOffer.setContractOffers(contractOffersMapped); @@ -113,27 +117,26 @@ private FetchedDataOffer dummyFetchedDataOffer(Do dataOffer) { return fetchedDataOffer; } - public String dummyAssetJson(Do dataOffer) { - var assetJsonLd = getAssetJsonLd( - UiAssetCreateRequest.builder() - .id(dataOffer.getAssetId()) - .title(dataOffer.getAssetTitle()) - .build() - ); - return JsonUtils.toJson(assetJsonLd); + public String dummyAssetJson(DataOfferWriterTestDataModels.Do dataOffer) { + var dummyUiAssetJson = Json.createObjectBuilder() + .add("assetId", dataOffer.getAssetId()) + .add("title", dataOffer.getAssetTitle()) + .add("assetJsonLd", "{}") + .build(); + return JsonUtils.toJson(dummyUiAssetJson); } public String dummyPolicyJson(String policyValue) { return "{\"%s\": \"%s\"}".formatted( - "SomePolicyField", policyValue + "SomePolicyField", policyValue ); } @NotNull - private FetchedContractOffer dummyFetchedContractOffer(Co it) { + private FetchedContractOffer dummyFetchedContractOffer(DataOfferWriterTestDataModels.Co it) { var contractOffer = new FetchedContractOffer(); contractOffer.setContractOfferId(it.getId()); - contractOffer.setPolicyJson(dummyPolicyJson(it.getPolicyValue())); + contractOffer.setUiPolicyJson(dummyPolicyJson(it.getPolicyValue())); return contractOffer; } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDataModels.java similarity index 95% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDataModels.java index ab15daccb..7dd824d99 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestDataModels.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDataModels.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; import lombok.Value; import lombok.With; diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java new file mode 100644 index 000000000..fb39bcbec --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestDydi.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.crawling.writing; + +import de.sovity.edc.ext.catalog.crawler.dao.CatalogPatchApplier; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferQueries; +import de.sovity.edc.ext.catalog.crawler.dao.contract_offers.ContractOfferRecordUpdater; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferQueries; +import de.sovity.edc.ext.catalog.crawler.dao.data_offers.DataOfferRecordUpdater; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; +import lombok.Value; +import org.eclipse.edc.spi.system.configuration.Config; + +import static org.mockito.Mockito.mock; + +@Value +class DataOfferWriterTestDydi { + Config config = mock(Config.class); + CrawlerConfig crawlerConfig = mock(CrawlerConfig.class); + DataOfferQueries dataOfferQueries = new DataOfferQueries(); + ContractOfferQueries contractOfferQueries = new ContractOfferQueries(); + ContractOfferRecordUpdater contractOfferRecordUpdater = new ContractOfferRecordUpdater(); + ConnectorQueries connectorQueries = new ConnectorQueries(crawlerConfig); + DataOfferRecordUpdater dataOfferRecordUpdater = new DataOfferRecordUpdater( + connectorQueries + ); + CatalogPatchBuilder catalogPatchBuilder = new CatalogPatchBuilder( + contractOfferQueries, + dataOfferQueries, + dataOfferRecordUpdater, + contractOfferRecordUpdater + ); + CatalogPatchApplier catalogPatchApplier = new CatalogPatchApplier(); + ConnectorUpdateCatalogWriter connectorUpdateCatalogWriter = new ConnectorUpdateCatalogWriter(catalogPatchBuilder, catalogPatchApplier); +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestResultHelper.java similarity index 78% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestResultHelper.java index 1b2f04c21..e5dfa39bb 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DataOfferWriterTestResultHelper.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DataOfferWriterTestResultHelper.java @@ -12,11 +12,11 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; -import de.sovity.edc.ext.brokerserver.db.jooq.Tables; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.ContractOfferRecord; -import de.sovity.edc.ext.brokerserver.db.jooq.tables.records.DataOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.Tables; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.ContractOfferRecord; +import de.sovity.edc.ext.catalog.crawler.db.jooq.tables.records.DataOfferRecord; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -33,8 +33,8 @@ class DataOfferWriterTestResultHelper { DataOfferWriterTestResultHelper(DSLContext dsl) { this.dataOffers = dsl.selectFrom(Tables.DATA_OFFER).fetchMap(Tables.DATA_OFFER.ASSET_ID); this.contractOffers = dsl.selectFrom(Tables.CONTRACT_OFFER).stream().collect(groupingBy( - ContractOfferRecord::getAssetId, - Collectors.toMap(ContractOfferRecord::getContractOfferId, Function.identity()) + ContractOfferRecord::getAssetId, + Collectors.toMap(ContractOfferRecord::getContractOfferId, Function.identity()) )); } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DiffUtilsTest.java similarity index 79% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DiffUtilsTest.java index 7a35e66db..79933dc2a 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/services/refreshing/offers/DiffUtilsTest.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/crawling/writing/DiffUtilsTest.java @@ -12,9 +12,9 @@ * */ -package de.sovity.edc.ext.brokerserver.services.refreshing.offers; +package de.sovity.edc.ext.catalog.crawler.crawling.writing; -import de.sovity.edc.ext.brokerserver.services.refreshing.offers.DiffUtils.DiffResultMatch; +import de.sovity.edc.ext.catalog.crawler.crawling.writing.utils.DiffUtils; import org.junit.jupiter.api.Test; import java.util.List; @@ -35,7 +35,7 @@ void testCompareLists() { // assert assertThat(actual.added()).containsExactly("3"); - assertThat(actual.updated()).containsExactly(new DiffResultMatch<>(1, "1")); + assertThat(actual.updated()).containsExactly(new DiffUtils.DiffResultMatch<>(1, "1")); assertThat(actual.removed()).containsExactly(2); } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRefTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRefTest.java new file mode 100644 index 000000000..6593a5397 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorRefTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.dao.connectors; + + +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +class ConnectorRefTest { + + @Test + void testEqualsTrue() { + // arrange + var a = new ConnectorRef("a", "1", "1", "1", "1"); + var b = new ConnectorRef("a", "2", "2", "2", "2"); + + // act + var result = a.equals(b); + + // assert + assertThat(result).isTrue(); + } + + @Test + void testEqualsFalse() { + // arrange + var a = new ConnectorRef("a", "1", "1", "1", "1"); + var b = new ConnectorRef("b", "1", "1", "1", "1"); + + // act + var result = a.equals(b); + + // assert + assertThat(result).isFalse(); + } + + @Test + void testSet() { + // arrange + var a = new ConnectorRef("a", "1", "1", "1", "1"); + var a2 = new ConnectorRef("a", "2", "2", "2", "2"); + var b = new ConnectorRef("b", "1", "1", "1", "1"); + + // act + var result = new HashSet<>(List.of(a, a2, b)).stream().map(ConnectorRef::getConnectorId).toList(); + + // assert + assertThat(result).containsExactlyInAnyOrder("a", "b"); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolQueueTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolQueueTest.java new file mode 100644 index 000000000..e6b1ad053 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/queue/ThreadPoolQueueTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.orchestration.queue; + +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.mockito.Mockito.mock; + +class ThreadPoolQueueTest { + + + /** + * Regression against bug where the queue did not act like a queue. + */ + @Test + void testOrdering() { + Runnable noop = () -> { + }; + + var c10 = mock(ConnectorRef.class); + var c20 = mock(ConnectorRef.class); + var c11 = mock(ConnectorRef.class); + var c21 = mock(ConnectorRef.class); + var c00 = mock(ConnectorRef.class); + + var queue = new ThreadPoolTaskQueue(); + queue.add(new ThreadPoolTask(1, noop, c10)); + queue.add(new ThreadPoolTask(2, noop, c20)); + queue.add(new ThreadPoolTask(1, noop, c11)); + queue.add(new ThreadPoolTask(2, noop, c21)); + queue.add(new ThreadPoolTask(0, noop, c00)); + + var result = new ArrayList(); + queue.getQueue().drainTo(result); + + Assertions.assertThat(result).extracting(ThreadPoolTask::getConnectorRef) + .containsExactly(c00, c10, c11, c20, c21); + } +} diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRemovalJobTest.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRemovalJobTest.java new file mode 100644 index 000000000..f054ff5b8 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/orchestration/schedules/OfflineConnectorRemovalJobTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.catalog.crawler.orchestration.schedules; + +import de.sovity.edc.ext.catalog.crawler.CrawlerTestDb; +import de.sovity.edc.ext.catalog.crawler.TestData; +import de.sovity.edc.ext.catalog.crawler.crawling.OfflineConnectorCleaner; +import de.sovity.edc.ext.catalog.crawler.crawling.logging.CrawlerEventLogger; +import de.sovity.edc.ext.catalog.crawler.dao.CatalogCleaner; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorQueries; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; +import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorStatusUpdater; +import de.sovity.edc.ext.catalog.crawler.db.jooq.enums.ConnectorOnlineStatus; +import de.sovity.edc.ext.catalog.crawler.orchestration.config.CrawlerConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.time.Duration; +import java.time.OffsetDateTime; + +import static de.sovity.edc.ext.catalog.crawler.db.jooq.tables.Connector.CONNECTOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class OfflineConnectorRemovalJobTest { + @RegisterExtension + private static final CrawlerTestDb TEST_DATABASE = new CrawlerTestDb(); + + ConnectorRef connectorRef = TestData.connectorRef; + + CrawlerConfig crawlerConfig; + OfflineConnectorCleaner offlineConnectorCleaner; + ConnectorQueries connectorQueries; + + @BeforeEach + void beforeEach() { + crawlerConfig = mock(CrawlerConfig.class); + connectorQueries = new ConnectorQueries(crawlerConfig); + offlineConnectorCleaner = new OfflineConnectorCleaner( + crawlerConfig, + new ConnectorQueries(crawlerConfig), + new CrawlerEventLogger(), + new ConnectorStatusUpdater(), + new CatalogCleaner() + ); + when(crawlerConfig.getEnvironmentId()).thenReturn(connectorRef.getEnvironmentId()); + } + + @Test + void test_offlineConnectorCleaner_should_be_dead() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + when(crawlerConfig.getKillOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); + TestData.insertConnector(dsl, connectorRef, record -> { + record.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + record.setLastSuccessfulRefreshAt(OffsetDateTime.now().minusDays(6)); + }); + + // act + offlineConnectorCleaner.cleanConnectorsIfOfflineTooLong(dsl); + + // assert + var connector = dsl.fetchOne(CONNECTOR, CONNECTOR.CONNECTOR_ID.eq(connectorRef.getConnectorId())); + assertThat(connector.getOnlineStatus()).isEqualTo(ConnectorOnlineStatus.DEAD); + }); + } + + @Test + void test_offlineConnectorCleaner_should_not_be_dead() { + TEST_DATABASE.testTransaction(dsl -> { + // arrange + when(crawlerConfig.getKillOfflineConnectorsAfter()).thenReturn(Duration.ofDays(5)); + TestData.insertConnector(dsl, connectorRef, record -> { + record.setOnlineStatus(ConnectorOnlineStatus.OFFLINE); + record.setLastSuccessfulRefreshAt(OffsetDateTime.now().minusDays(2)); + }); + + // act + offlineConnectorCleaner.cleanConnectorsIfOfflineTooLong(dsl); + + // assert + var connector = dsl.fetchOne(CONNECTOR, CONNECTOR.CONNECTOR_ID.eq(connectorRef.getConnectorId())); + assertThat(connector.getOnlineStatus()).isEqualTo(ConnectorOnlineStatus.OFFLINE); + }); + } + +} diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CollectionUtils2Test.java similarity index 62% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CollectionUtils2Test.java index 2b5e926a8..87673188f 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/CollectionUtils2Test.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/CollectionUtils2Test.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import org.junit.jupiter.api.Test; @@ -41,20 +41,4 @@ void isNotEmpty_withNull() { void isNotEmpty_withNonEmptyList() { assertThat(CollectionUtils2.isNotEmpty(List.of(1))).isTrue(); } - - - @Test - void allElementsWithoutGivenIndex_start() { - assertThat(CollectionUtils2.allElementsExceptForIndex(List.of("A", "B", "C", "D"), 0)).containsExactly("B", "C", "D"); - } - - @Test - void allElementsWithoutGivenIndex_middle() { - assertThat(CollectionUtils2.allElementsExceptForIndex(List.of("A", "B", "C", "D"), 2)).containsExactly("A", "B", "D"); - } - - @Test - void allElementsWithoutGivenIndex_end() { - assertThat(CollectionUtils2.allElementsExceptForIndex(List.of("A", "B", "C", "D"), 3)).containsExactly("A", "B", "C"); - } } diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/JsonUtils2Test.java similarity index 95% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/JsonUtils2Test.java index 1279a31be..8843aebbb 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/JsonUtils2Test.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/JsonUtils2Test.java @@ -12,7 +12,7 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import org.junit.jupiter.api.Test; diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/StringUtils2Test.java similarity index 68% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java rename to extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/StringUtils2Test.java index 70f415ae5..1cbd56dd5 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/utils/StringUtils2Test.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/StringUtils2Test.java @@ -12,12 +12,10 @@ * */ -package de.sovity.edc.ext.brokerserver.utils; +package de.sovity.edc.ext.catalog.crawler.utils; import org.junit.jupiter.api.Test; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; class StringUtils2Test { @@ -43,20 +41,4 @@ void removeSuffix_withoutSuffix() { assertThat(StringUtils2.removeSuffix("test", "abc")).isEqualTo("test"); } - - @Test - void lowercaseWords_emptyString() { - assertThat(StringUtils2.lowercaseWords("")).isEmpty(); - } - - @Test - void lowercaseWords_blankString() { - assertThat(StringUtils2.lowercaseWords(" ")).isEmpty(); - } - - - @Test - void lowercaseWords_someWords() { - assertThat(StringUtils2.lowercaseWords(" a \n\t B a ")).isEqualTo(List.of("a", "b", "a")); - } } diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/resources/logging.properties b/extensions/catalog-crawler/catalog-crawler/src/test/resources/logging.properties new file mode 100644 index 000000000..471bd20d6 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler/src/test/resources/logging.properties @@ -0,0 +1,6 @@ +.level=ALL +org.eclipse.edc.level=ALL +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=[%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS] [%4$-7s] %5$s%6$s%n diff --git a/extensions/broker-server-postgres-flyway-jooq/README.md b/extensions/postgres-flyway-core/README.md similarity index 56% rename from extensions/broker-server-postgres-flyway-jooq/README.md rename to extensions/postgres-flyway-core/README.md index ac9be06e0..8c0cb00da 100644 --- a/extensions/broker-server-postgres-flyway-jooq/README.md +++ b/extensions/postgres-flyway-core/README.md @@ -5,7 +5,7 @@ Logo -

      Broker Server:
      PostgreSQL + Flyway + JooQ

      +

      EDC-Connector Extension:
      Flyway + Hikari Utils

      Report Bug @@ -14,13 +14,15 @@

      -## About this Extension Package +## About this Module -This module contains: +Flyway and Hikari common code used between our EDC Launchers, tests and the Crawler. -- An EDC Extension migrating the db schema on startup. -- The entire Broker Server DB Schema as JooQ generated code. -- A `DslContextFactory` to quickly start using the JooQ generated code. +It is un-opinionated regarding the location of the migration scripts and does not provide an EDC extension. + +## Why does this extension exist? + +Programmatically calling Flyways migrations is verbose, and unfortunately our EDC Flyway extension is riddled with legacy code. ## License diff --git a/extensions/postgres-flyway-core/build.gradle.kts b/extensions/postgres-flyway-core/build.gradle.kts new file mode 100644 index 000000000..5e8b2ef2f --- /dev/null +++ b/extensions/postgres-flyway-core/build.gradle.kts @@ -0,0 +1,26 @@ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + api(libs.flyway.core) + api(libs.postgres) + api(libs.hikari) + + implementation(libs.apache.commonsLang) +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java new file mode 100644 index 000000000..f56da9d6b --- /dev/null +++ b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql; + +import lombok.Builder; +import lombok.Singular; +import lombok.Value; +import lombok.With; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +@Builder +@Value +@With +public class FlywayExecutionParams { + @Builder.Default + Consumer infoLogger = System.out::println; + + @Builder.Default + List migrationLocations = new ArrayList<>(); + + @Builder.Default + boolean migrate = false; + + @Builder.Default + boolean tryRepairOnFailedMigration = false; + + @Builder.Default + boolean cleanEnabled = false; + + @Builder.Default + boolean clean = false; + + @Builder.Default + String table = "flyway_schema_history"; +} diff --git a/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayUtils.java b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayUtils.java new file mode 100644 index 000000000..be116cc23 --- /dev/null +++ b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial implementation + * + */ + +package de.sovity.edc.extension.postgresql; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.output.MigrateResult; +import org.flywaydb.core.api.output.RepairResult; + +import java.util.Arrays; +import java.util.List; +import javax.sql.DataSource; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class FlywayUtils { + private final FlywayExecutionParams params; + private final DataSource dataSource; + + public static void cleanAndMigrate(FlywayExecutionParams params, DataSource dataSource) { + var instance = new FlywayUtils(params, dataSource); + instance.cleanIfEnabled(); + instance.migrateOrValidate(); + } + + public static List parseFlywayLocations(String locations) { + return Arrays.stream(locations.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .toList(); + } + + private void cleanIfEnabled() { + if (params.isClean()) { + params.getInfoLogger().accept("Running flyway clean."); + var flyway = setupFlyway(); + flyway.clean(); + } + } + + private void migrateOrValidate() { + if (params.isMigrate()) { + migrate(); + } else { + validate(); + } + } + + private void validate() { + var flyway = setupFlyway(); + flyway.validate(); + } + + private void migrate() { + var flyway = setupFlyway(); + flyway.info().getInfoResult().migrations.stream() + .map(migration -> "Found migration: %s".formatted(migration.filepath)) + .forEach(it -> params.getInfoLogger().accept(it)); + + try { + var migrateResult = flyway.migrate(); + handleFlywayMigrationResult(migrateResult); + } catch (FlywayException e) { + if (params.isTryRepairOnFailedMigration()) { + repairAndRetryMigration(flyway); + } else { + throw new IllegalStateException("Flyway migration failed for '%s'" + .formatted(params.getTable()), e); + } + } + } + + private void repairAndRetryMigration(Flyway flyway) { + try { + var repairResult = flyway.repair(); + handleFlywayRepairResult(repairResult); + var migrateResult = flyway.migrate(); + handleFlywayMigrationResult(migrateResult); + } catch (FlywayException e) { + throw new IllegalStateException("Flyway migration failed for '%s'" + .formatted(params.getTable()), e); + } + } + + private void handleFlywayRepairResult(RepairResult repairResult) { + if (!repairResult.repairActions.isEmpty()) { + var repairActions = String.join(", ", repairResult.repairActions); + params.getInfoLogger().accept("Repair actions for datasource %s: %s" + .formatted(params.getTable(), repairActions)); + } + + if (!repairResult.warnings.isEmpty()) { + var warnings = String.join(", ", repairResult.warnings); + throw new IllegalStateException("Repairing datasource %s failed: %s" + .formatted(params.getTable(), warnings)); + } + } + + private Flyway setupFlyway() { + params.getInfoLogger().accept("Flyway migration locations for '%s': %s".formatted( + params.getTable(), params.getMigrationLocations())); + return Flyway.configure() + .baselineOnMigrate(true) + .failOnMissingLocations(true) + .dataSource(dataSource) + .table(params.getTable()) + .locations(params.getMigrationLocations().toArray(new String[0])) + .cleanDisabled(!params.isCleanEnabled()) + .load(); + } + + private void handleFlywayMigrationResult(MigrateResult migrateResult) { + if (migrateResult.migrationsExecuted > 0) { + params.getInfoLogger().accept(String.format( + "Successfully migrated database for datasource %s " + + "from version %s to version %s", + params.getTable(), + migrateResult.initialSchemaVersion, + migrateResult.targetSchemaVersion)); + } else { + params.getInfoLogger().accept(String.format( + "No migration necessary for datasource %s. Current version is %s", + params.getTable(), + migrateResult.initialSchemaVersion)); + } + } +} diff --git a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/HikariDataSourceFactory.java similarity index 59% rename from extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java rename to extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/HikariDataSourceFactory.java index 70abc53e8..bfbb7d3be 100644 --- a/extensions/broker-server-postgres-flyway-jooq/src/main/java/de/sovity/edc/ext/brokerserver/db/DataSourceFactory.java +++ b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/HikariDataSourceFactory.java @@ -12,40 +12,23 @@ * */ -package de.sovity.edc.ext.brokerserver.db; +package de.sovity.edc.extension.postgresql; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; -import de.sovity.edc.ext.brokerserver.db.utils.JdbcCredentials; -import lombok.RequiredArgsConstructor; -import org.eclipse.edc.spi.system.configuration.Config; +import lombok.experimental.UtilityClass; import javax.sql.DataSource; -@RequiredArgsConstructor -public class DataSourceFactory { - private final Config config; - - - /** - * Create a new {@link DataSource} from EDC Config. - * - * @return {@link DataSource}. - */ - public HikariDataSource newDataSource() { - var jdbcCredentials = JdbcCredentials.fromConfig(config); - int maxPoolSize = config.getInteger(PostgresFlywayExtension.DB_CONNECTION_POOL_SIZE); - int connectionTimeoutInMs = config.getInteger(PostgresFlywayExtension.DB_CONNECTION_TIMEOUT_IN_MS); - return newDataSource(jdbcCredentials, maxPoolSize, connectionTimeoutInMs); - } - +@UtilityClass +public class HikariDataSourceFactory { /** * Create a new {@link DataSource}. *
      * This method is static, so we can use from test code. * - * @param jdbcCredentials jdbc credentials - * @param maxPoolSize max pool size + * @param jdbcCredentials jdbc credentials + * @param maxPoolSize max pool size * @param connectionTimeoutInMs connection timeout in ms * @return {@link DataSource}. */ @@ -61,7 +44,7 @@ public static HikariDataSource newDataSource( hikariConfig.setMinimumIdle(1); hikariConfig.setMaximumPoolSize(maxPoolSize); hikariConfig.setIdleTimeout(30000); - hikariConfig.setPoolName("edc-broker-server"); + hikariConfig.setPoolName("edc-server"); hikariConfig.setMaxLifetime(50000); hikariConfig.setConnectionTimeout(connectionTimeoutInMs); diff --git a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/JdbcCredentials.java similarity index 53% rename from extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java rename to extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/JdbcCredentials.java index 31126e7eb..e5cae2227 100644 --- a/extensions/broker-server/src/test/java/de/sovity/edc/ext/brokerserver/db/TestDatabaseCancelTransactionException.java +++ b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/JdbcCredentials.java @@ -12,8 +12,18 @@ * */ -package de.sovity.edc.ext.brokerserver.db; - -public class TestDatabaseCancelTransactionException extends RuntimeException { +package de.sovity.edc.extension.postgresql; +/** + * JDBC Credentials + * + * @param jdbcUrl JDBC URL without credentials + * @param jdbcUser JDBC User + * @param jdbcPassword JDBC Password + */ +public record JdbcCredentials( + String jdbcUrl, + String jdbcUser, + String jdbcPassword +) { } diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java index 357892532..28afc4ae9 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/PostgresFlywayExtension.java @@ -103,4 +103,10 @@ public void initialize(ServiceExtensionContext context) { migrator.updateDatabaseWithLegacyHandling(); } + @Override + public void shutdown() { + if (dataSource != null) { + dataSource.close(); + } + } } diff --git a/extensions/wrapper/README.md b/extensions/wrapper/README.md index 0d463cde7..049416077 100644 --- a/extensions/wrapper/README.md +++ b/extensions/wrapper/README.md @@ -41,8 +41,8 @@ Our EDC API Wrapper APIs and API Clients are compatible with both our sovity EDC - [TypeScript API Client Library](./clients/typescript-client) - [TypeScript API Client Library Example](./clients/typescript-client-example) - Utilities: - - Broker UI / Connector UI [Common Models](./wrapper-common-api) - - Broker / Connector [Common Services](./wrapper-common-mappers) + - Opinionated and simplified Asset and Policy [Models](./wrapper-common-api) + - Opinionated and simplified Asset and Policy [Mappers](./wrapper-common-mappers) ## License diff --git a/extensions/wrapper/wrapper-common-api/README.md b/extensions/wrapper/wrapper-common-api/README.md index 3194a827f..5a0f6b0a5 100644 --- a/extensions/wrapper/wrapper-common-api/README.md +++ b/extensions/wrapper/wrapper-common-api/README.md @@ -16,8 +16,7 @@ ## About this module -Common API models between the sovity Community Edition EDC API, sovity Enterprise Edition EDC API and/or the Broker -Server API. +Common API models between the sovity Community Edition EDC API, sovity Enterprise Edition EDC API and/or the Authority Portal. ## Why does this module exist? diff --git a/extensions/wrapper/wrapper-common-mappers/README.md b/extensions/wrapper/wrapper-common-mappers/README.md index 5020b55b5..62a63b282 100644 --- a/extensions/wrapper/wrapper-common-mappers/README.md +++ b/extensions/wrapper/wrapper-common-mappers/README.md @@ -17,13 +17,12 @@ ## About this module Common API models naooers between the sovity Community Edition EDC API, sovity Enterprise Edition EDC API and/or the -Broker -Server API. +Authority Portal. ## Why does this module exist? Our common API models defined in [wrapper-common-api](../wrapper-common-api) need mapping to and from the core EDC -types in both the EDC CE, EDC EE and Broker Server. +types in both the EDC CE, EDC EE and Authority Portal. ## License diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java index a72e1ddad..30340044c 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiService.java @@ -35,6 +35,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.BinaryOperator; import java.util.function.Function; @@ -60,7 +61,7 @@ public class TransferHistoryPageApiService { public List getTransferHistoryEntries() { var negotiationsById = getAllContractNegotiations().stream() - .filter(negotiation -> negotiation != null) + .filter(Objects::nonNull) .filter(negotiation -> negotiation.getContractAgreement() != null) .collect(toMap( it -> it.getContractAgreement().getId(), @@ -98,7 +99,7 @@ public List getTransferHistoryEntries() { } agreement.ifPresent(it -> transferHistoryEntry.setContractAgreementId(it.getId())); - negotiation.ifPresent( it -> { + negotiation.ifPresent(it -> { transferHistoryEntry.setCounterPartyConnectorEndpoint(it.getCounterPartyAddress()); transferHistoryEntry.setCounterPartyParticipantId(it.getCounterPartyId()); transferHistoryEntry.setCreatedDate(utcMillisToOffsetDateTime(it.getCreatedAt())); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java index d9edd6444..2156b609a 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java @@ -28,6 +28,7 @@ import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; import org.eclipse.edc.spi.entity.Entity; import org.eclipse.edc.spi.query.QuerySpec; import org.junit.jupiter.api.BeforeEach; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 573e48829..398d77f68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # groups edcGroup="org.eclipse.edc" -sovityBrokerServerGroup = "de.sovity.broker" +sovityCatalogCrawlerGroup = "de.sovity.edc.catalog.crawler" sovityEdcExtensionGroup = "de.sovity.edc.ext" sovityEdcGroup = "de.sovity.edc" diff --git a/launchers/.env.broker b/launchers/.env.catalog-crawler similarity index 58% rename from launchers/.env.broker rename to launchers/.env.catalog-crawler index cc006d7cd..77aa13f53 100644 --- a/launchers/.env.broker +++ b/launchers/.env.catalog-crawler @@ -5,66 +5,55 @@ # - Watch out for escaping issues as values will be surrounded by quotes, and dollar signs must be escaped. # =========================================================== -# Available Broker Server Config +# Available Catalog Crawler Config # =========================================================== +# Environment ID +CRAWLER_ENVIRONMENT_ID=missing-env-CRAWLER_ENVIRONMENT_ID + # Fully Qualified Domain Name (e.g. example.com) MY_EDC_FQDN=missing-env-MY_EDC_FQDN # Postgres Database Connection -MY_EDC_JDBC_URL=jdbc:postgresql://missing-postgresql-url -MY_EDC_JDBC_USER=missing-postgresql-user -MY_EDC_JDBC_PASSWORD=missing-postgresql-password - -# Broker Server Admin Api Key (required) -# This is a stopgap until we have IAM -EDC_BROKER_SERVER_ADMIN_API_KEY=DefaultBrokerServerAdminApiKey +CRAWLER_DB_JDBC_URL=jdbc:postgresql://missing-postgresql-url +CRAWLER_DB_JDBC_USER=missing-postgresql-user +CRAWLER_DB_JDBC_PASSWORD=missing-postgresql-password # Database Connection Pool Size -EDC_BROKER_SERVER_DB_CONNECTION_POOL_SIZE=30 +CRAWLER_DB_CONNECTION_POOL_SIZE=30 # Database Connection Timeout (in ms) -EDC_BROKER_SERVER_DB_CONNECTION_TIMEOUT_IN_MS=30000 - -# List of Connectors to be added on startup -EDC_BROKER_SERVER_KNOWN_CONNECTORS= - -# Default Data Space Name -EDC_BROKER_SERVER_DEFAULT_DATASPACE=MDS - -# List of Data Space Names for special Connectors -# e.g. Mobilithek=https://my-connector1/api/dsp,SomeOtherDataspace=https://my-connector2/api/dsp -EDC_BROKER_SERVER_KNOWN_DATASPACE_CONNECTORS= +CRAWLER_DB_CONNECTION_TIMEOUT_IN_MS=30000 # CRON interval for crawling ONLINE connectors -EDC_BROKER_SERVER_CRON_ONLINE_CONNECTOR_REFRESH=*/20 * * ? * * +CRAWLER_CRON_ONLINE_CONNECTOR_REFRESH=*/20 * * ? * * # CRON interval for crawling OFFLINE connectors -EDC_BROKER_SERVER_CRON_OFFLINE_CONNECTOR_REFRESH=0 */5 * ? * * +CRAWLER_CRON_OFFLINE_CONNECTOR_REFRESH=0 */5 * ? * * # CRON interval for crawling DEAD connectors -EDC_BROKER_SERVER_CRON_DEAD_CONNECTOR_REFRESH=0 0 * ? * * +CRAWLER_CRON_DEAD_CONNECTOR_REFRESH=0 0 * ? * * # CRON interval for marking connectors as DEAD -EDC_BROKER_SERVER_SCHEDULED_KILL_OFFLINE_CONNECTORS=0 0 2 ? * * +CRAWLER_SCHEDULED_KILL_OFFLINE_CONNECTORS=0 0 2 ? * * # Delete data offers / mark as dead after connector has been offline for: -EDC_BROKER_SERVER_KILL_OFFLINE_CONNECTORS_AFTER=P5D +CRAWLER_KILL_OFFLINE_CONNECTORS_AFTER=P5D # Hide data offers after connector has been offline for: -EDC_BROKER_SERVER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D +CRAWLER_HIDE_OFFLINE_DATA_OFFERS_AFTER=P1D # Parallelization for Crawling -EDC_BROKER_SERVER_NUM_THREADS=32 +CRAWLER_NUM_THREADS=32 # Maximum number of Data Offers per Connector -EDC_BROKER_SERVER_MAX_DATA_OFFERS_PER_CONNECTOR=50 +CRAWLER_MAX_DATA_OFFERS_PER_CONNECTOR=50 # Maximum number of Contract Offers per Data Offer -EDC_BROKER_SERVER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER=10 +CRAWLER_MAX_CONTRACT_OFFERS_PER_DATA_OFFER=10 -# Pagination Configuration: Catalog Page Size -EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 +# Enable the extension +CRAWLER_EXTENSION_ENABLED=true # =========================================================== # Other EDC Config @@ -74,7 +63,7 @@ EDC_BROKER_SERVER_CATALOG_PAGE_PAGE_SIZE=20 MY_EDC_PARTICIPANT_ID=broker EDC_CONNECTOR_NAME=${MY_EDC_PARTICIPANT_ID:-MY_EDC_NAME_KEBAB_CASE} EDC_PARTICIPANT_ID=${MY_EDC_PARTICIPANT_ID:-MY_EDC_NAME_KEBAB_CASE} -MY_EDC_BASE_PATH=/backend +MY_EDC_BASE_PATH= MY_EDC_PROTOCOL=https:// WEB_HTTP_PORT=11001 WEB_HTTP_MANAGEMENT_PORT=11002 @@ -85,16 +74,9 @@ WEB_HTTP_MANAGEMENT_PATH=${MY_EDC_BASE_PATH}/api/management WEB_HTTP_PROTOCOL_PATH=${MY_EDC_BASE_PATH}/api/dsp WEB_HTTP_CONTROL_PATH=${MY_EDC_BASE_PATH}/api/control -EDC_CONNECTOR_NAME=$MY_EDC_NAME_KEBAB_CASE EDC_HOSTNAME=${MY_EDC_FQDN} EDC_DSP_CALLBACK_ADDRESS=${MY_EDC_PROTOCOL}${MY_EDC_FQDN}${WEB_HTTP_PROTOCOL_PATH} -# Flyway Extension: Defaults -EDC_DATASOURCE_DEFAULT_NAME=default -EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL -EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER -EDC_DATASOURCE_DEFAULT_PASSWORD=$MY_EDC_JDBC_PASSWORD - # Oauth default configurations for compatibility with sovity DAPS EDC_OAUTH_PROVIDER_AUDIENCE=${EDC_OAUTH_TOKEN_URL} EDC_OAUTH_ENDPOINT_AUDIENCE=idsc:IDS_CONNECTORS_ALL diff --git a/launchers/README.md b/launchers/README.md index 9a1f60f3d..dd9d43c90 100644 --- a/launchers/README.md +++ b/launchers/README.md @@ -100,7 +100,6 @@ Our sovity Community Edition EDC is built as several docker image variants in di
    1. Management API Auth via API Keys
    2. PostgreSQL Persistence & Flyway
    3. DAPS Authentication
    4. -
    5. Broker Extension
    6. Clearing House Extension
    7. - broker-dev + catalog-crawler-dev Development
        -
      • Local Demo via our - docker-compose.yaml -
      • +
      • Local Demo
      • E2E Testing
        -
      • Broker Server Extension(s)
      • -
      • PostgreSQL Persistence & Flyway
      • +
      • Catalog Crawler for one environment
      • Mock IAM
      broker-cecatalog-crawler-ce Community Edition
        @@ -163,8 +159,7 @@ Our sovity Community Edition EDC is built as several docker image variants in di
        -
      • Broker Server Extension(s)
      • -
      • PostgreSQL Persistence & Flyway
      • +
      • Catalog Crawler for one environment
      • DAPS Authentication
      Launch two sovity EDC CE ConnectorsLaunch two MDS EDC CELaunch two MDS EDC CE Connectors
      - - - - - - - - - - - - -
      Launch two sovity EDC CE ConnectorsLaunch two MDS EDC CE
      - -```shell script -# Run with Bash from the root directory of the project -# Use the release tag 4.2.0: https://github.com/sovity/edc-ce/releases/tag/v4.2.0 - -# Log-In to the Github Container Registry -docker login ghcr.io - -# Start sovity EDC Connectors -docker compose up -``` - - - -```shell script -# Run with Bash from the root directory of the project -# Use the release tag 4.2.0: https://github.com/sovity/edc-ce/releases/tag/v4.2.0 - -# Log-In to the Github Container Registry -docker login ghcr.io - -# Start MDS EDC Connectors -EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up -``` - -
      - -## Quick Start: Default Configuration - -The default configuration launches two local EDC Connectors with the following credentials: - -| | First Connector | Second Connector | -|---------------------|-----------------------------------------------------------------------|:--------------------------------------------------------------------------------| -| Homepage | http://localhost:11000 | http://localhost:22000 | -| Management Endpoint | http://localhost:11002/api/v1/management | http://localhost:22002/api/v1/management | -| Management API Key | `ApiKeyDefaultValue` | `ApiKeyDefaultValue` | -| Connector Endpoint | http://edc:11003/api/v1/ids/data
      Requires Docker Compose Network | http://edc2:22003/api/v1/ids/data
      Requires Docker Compose Network | diff --git a/docs/deployment-guide/goals/local-demo/README.md b/docs/deployment-guide/goals/local-demo/README.md index d52d6c1f5..7d15efd51 100644 --- a/docs/deployment-guide/goals/local-demo/README.md +++ b/docs/deployment-guide/goals/local-demo/README.md @@ -1,8 +1,6 @@ Deployment Goal: Local Demo ======== -> This is for our latest version. There is another guide for [4.2.0](4.2.0/README.md). - > On how to deploy a productive connector with joining an existing Data Space, please refer > to our [Productive Deployment Guide](../production/README.md). @@ -15,7 +13,7 @@ start [docker-compose.yaml](../../../../docker-compose.yaml) file. Launch two sovity EDC CE Connectors -Launch two MDS EDC CE +Launch two MDS EDC CE Connectors diff --git a/docs/deployment-guide/goals/production/4.2.0/README.md b/docs/deployment-guide/goals/production/4.2.0/README.md deleted file mode 100644 index ee9f58d48..000000000 --- a/docs/deployment-guide/goals/production/4.2.0/README.md +++ /dev/null @@ -1,182 +0,0 @@ -Deployment Goal: Production -======== - -> This is for an old major version sovity EDC CE 4.2.0. [Go back](../README.md) - -## About this Guide - -This is a productive deployment guide for self-hosting a functional sovity CE EDC Connector or MDS CE EDC Connector. - -## Requirements - -A productive EDC Connector deployment has strict requirements, with slight errors in configuration already causing -contract negotiations / data transfer to fail. - -In general a productive EDC Connector requires a DAPS Server, DAPS Credentials, a reverse proxy configured in detail due -to technical reasons, reachability via the internet and well-defined URLs across all configurations. - -## Deployment Units - -To deploy an EDC multiple deployment units must be deployed and configured. - -| Deployment Unit | Version / Details | -|----------------------------------------------------------------|------------------------------------------------------------------------------------------------| -| An Auth Proxy / Auth solution of your choice. | (deployment specific, required to secure UI and management API) | -| Reverse Proxy that merges the UI+Backend and removes the ports | (deployment specific) | -| Postgresql | 13 or compatible version | -| EDC Backend | edc-ce or edc-ce-mds, see [CHANGELOG.md](../../../../../CHANGELOG.md) for compatible versions. | -| EDC UI | edc-ui, see [CHANGELOG.md](../../../../../CHANGELOG.md) for compatible versions. | - -## Configuration - -### Reverse Proxy Configuration - -To make the deployment work, the connector needs to be exposed to the internet. Connector-to-Connector -communication is asynchronous and done with authentication via the DAPS. Thus, if the target connector cannot reach -your connector under its self-declared URLs, contract negotiation and transfer processes will fail. - -The EDC Backend opens up multiple ports with different functionalities. They are expected to be merged by a reverse -proxy (at least the protocol endpoint needs to be). - -- The sovity EDC Connector is meant to be deployed with a reverse proxy merging the following ports: - - The UI's `80` port. Henceforth, called the UI. - - The Backend's `11002` port. Henceforth, called the Management API. - - The Backend's `11003` port. Henceforth, called the Protocol API. -- The mapping should look like this: - - `/api/v1/ids` -> `edc:11003/api/v1/ids` - - `/api/v1/management` -> `edc:11002/api/v1/management` - - All other requests should be mapped to `edc-ui:80` -- Regarding TLS/HTTPS: - - All endpoints need to be secured by TLS/HTTPS. A productive connector won't work without it. - - The UI and the Management API should have HTTP to HTTPS redirects. - - The Protocol API must allow HTTP traffic to pass through. This is due to some loopback requests - mistakenly using HTTP instead of HTTPS that would otherwise be blocked or have their credentials wiped. -- Regarding Authentication: - - The UI and the Management API need to be secured by an auth proxy. Otherwise, access to either would mean full - control of the application. - - The backend's `11003` port needs to be unsecured. Authentication between connectors is done via the Data Space - Authority / DAPS and the configured certificates. -- Exposing to the internet: - - The Protocol API must be reachable via the internet. The required endpoints can be found in - this [public-endpoints.yaml](public-endpoints.yaml) - - Exposing the UI or the Management Endpoint to the internet requires an intermediate auth proxy, we recommend restricting the access to the Management Endpoint to your internal network. -- Security: - - Limit the header size in the proxy so that only a certain number of API Keys can be tested with one API-request (e.g. limit to 8kb). - - Limit the access rate to the API endpoints and monitor access for attacks like brute force attacks. - -## EDC UI Configuration - -A sovity EDC UI deployment requires the following config properties: - -```yaml -# Active Profile -EDC_UI_ACTIVE_PROFILE: sovity-open-source (or mds-open-source) - -# Management API URL -EDC_UI_DATA_MANAGEMENT_API_URL: https://[EDC URL]/api/v1/management - -# Management API Key -EDC_UI_DATA_MANAGEMENT_API_KEY: "ApiKeyDefaultValue" - -# Enable config fetching from the backend -EDC_UI_CONFIG_URL: "edc-ui-config" -``` - -## EDC Backend Configuration - -A sovity EDC CE or MDS EDC CE Backend deployment requires: - -- A running DAPS -- (MDS Only) A running Clearing House -- DAPS Access - and [a generated SKI/AKI pair and .jks file](#faq) -- The following configuration properties - -> [!WARNING] -> Please be careful with overriding any of the ENV Vars set in our [launchers/.env.connector](../../../../../launchers/.env.connector). -> Our defaults will respect overrides, and the Core EDC ENV Vars can be in some cases sensitive to edge cases such as -> trailing slashes. - -```yaml -# Connector Host Name -MY_EDC_FQDN: "my-edc-deployment1.example.com" - -# Connector Technical Name -MY_EDC_NAME_KEBAB_CASE: "example-connector" - -# Connector Localized Name / Title -MY_EDC_TITLE: "EDC Connector" - -# Connector Description Text -MY_EDC_DESCRIPTION: "sovity Community Edition EDC Connector" - -# Connector Curator -# The company using the EDC Connector, configuring data offers, etc. -MY_EDC_CURATOR_URL: "https://example.com" -MY_EDC_CURATOR_NAME: "Example GmbH" - -# Database Connection -MY_EDC_JDBC_URL: jdbc:postgresql://postgresql:5432/edc -MY_EDC_JDBC_USER: edc -MY_EDC_JDBC_PASSWORD: edc - -# Management API Key -# high entropy recommended when configuring the value (length, complexity, e.g. [a-zA-Z0-9+special chars]{32+ chars}) -EDC_API_AUTH_KEY: ApiKeyDefaultValue - -# Connector Maintainer -# The company hosting the EDC Connector -MY_EDC_MAINTAINER_URL: "https://sovity.de" -MY_EDC_MAINTAINER_NAME: "sovity GmbH" - -# (MDS Only) Clearing House -EDC_CLEARINGHOUSE_LOG_URL: 'https://clearing.test.mobility-dataspace.eu/messages/log' - -# DAPS URL -EDC_OAUTH_TOKEN_URL: 'https://daps.test.mobility-dataspace.eu/token' -EDC_OAUTH_PROVIDER_JWKS_URL: 'https://daps.test.mobility-dataspace.eu/jwks.json' - -# DAPS Credentials -EDC_OAUTH_CLIENT_ID: '_your SKI/AKI_' -EDC_KEYSTORE: '_path to .jks file in container_' -EDC_KEYSTORE_PASSWORD: '_your keystore password_' -EDC_OAUTH_CERTIFICATE_ALIAS: 1 -EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 -``` - -## FAQ - -### What should the client ID entry look like? - -Example of a client-ID entry: - -`EDC_OAUTH_CLIENT_ID: 7X:7Y:...:B2:94:keyid:6A:2B:...:28:80` - -### How do you get the SKI and AKI of a p12 and how do you convert it to a jks? - -You can use a script (if you're on WSL or Linux) to generate the SKI, AKI and jks file. - -1. Make sure you're on Linux or on a bash console (e.g. WSL or Git Bash) and have openssl and keytool installed -2. Navigate in the console to the resources/docs directory -3. Run the [script](../generate_ski_aki.sh) `./generate_ski_aki.sh [filepath].p12 [password]` and - substitute `[filepath]` to the p12 certificate - filepath and `[password]` to the certificate password -4. The jks file will be generated in the same folder as your p12 file and the SKI/AKI combination is printed out in the - console. - Copy the SKI:AKI combination and use it to start the EDC (optionally also save it to your password manager). -5. The generated `.jks` file needs to be mounted into your productive running container. - -### Can I run a connector locally and consume data from an online connector? - -No, locally run connectors cannot exchange data with online connectors. A connector must have a proper URL + -configuration and be accessible from the data provider via REST calls. - -### (MDS Only) Can I disable the ClearingHouse-Client-Extensions dynamically? - -Yes, if the two extensions are included, they can still be disabled via properties. -The default settings can be found in `docker-compose.yaml` and can be changed there. - -```yaml -# Extension Configuration -CLEARINGHOUSE_CLIENT_EXTENSION_ENABLED: true # enabled by default -``` diff --git a/docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml b/docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml deleted file mode 100644 index 421213278..000000000 --- a/docs/deployment-guide/goals/production/4.2.0/public-endpoints.yaml +++ /dev/null @@ -1,30 +0,0 @@ -openapi: 3.0.1 -info: - title: sovity EDC CE Public Endpoints - description: Required publicly exposed EDC Connector endpoints - version: 4.2.0 -servers: - - url: https://[MY_EDC_FQDN] -tags: - - name: Protocol API - description: Port 11003 on the Backend Container. -paths: - /api/v1/ids/data: - post: - tags: - - Protocol API - description: IDS Message Endpoint - requestBody: - content: - multipart/form-data: - schema: - type: object - responses: - '200': - description: OK or empty response if token validation failed - content: - multipart/form-data: - schema: - type: object - - diff --git a/docs/deployment-guide/goals/production/README.md b/docs/deployment-guide/goals/production/README.md index be879d009..b679811e6 100644 --- a/docs/deployment-guide/goals/production/README.md +++ b/docs/deployment-guide/goals/production/README.md @@ -1,7 +1,5 @@ # Productive Deployment Guide -> This is for our latest version. There is another guide for [4.2.0](4.2.0/README.md). - ## About this Guide This is a productive deployment guide for self-hosting a functional sovity CE EDC Connector or MDS CE EDC Connector. diff --git a/extensions/catalog-crawler/README.md b/extensions/catalog-crawler/README.md index 28de136de..881dbbf40 100644 --- a/extensions/catalog-crawler/README.md +++ b/extensions/catalog-crawler/README.md @@ -18,6 +18,7 @@ The catalog crawler is a deployment unit depending on an existing Authority Portal's database: +- It is a modified EDC connector with the task to crawl the other connector's public data offers. - It periodically checks the Authority Portal's connector list for its environment. - It crawls the given connectors in regular intervals. - It writes the data offers and connector statuses back into the Authority Portal DB. @@ -27,9 +28,13 @@ The catalog crawler is a deployment unit depending on an existing Authority Port The Authority Portal uses a non-EDC stack, and the EDC stack cannot handle multiple sources of authority at once. -With the `DB -> UI` part of the broker having been moved to the Authority Portal, only the `Catalog -> DB` part remains as the Catalog Crawler, +With the `DB -> UI` part of the broker having been moved to the Authority Portal, only the `Catalog -> DB` part remains as the Catalog Crawler, as it requires Connector-to-Connector IAM within the given Dataspace. +## Deployment + +Please see the [Catalog Crawler Productive Deployment Guide](../../docs/deployment-guide/goals/catalog-crawler-production/README.md) for more information. + ## License Apache License 2.0 - see [LICENSE](../../LICENSE) diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java index efcdb4ab6..fc92c506e 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java @@ -47,7 +47,7 @@ @ApiTest @ExtendWith(EdcExtension.class) -public class UseCaseApiWrapperTest { +class UseCaseApiWrapperTest { private EdcClient client; private String assetId1 = "test-asset-1"; From 516e101273076ce80d52abae26772990af4f9d0e Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 18 Jul 2024 12:08:03 +0200 Subject: [PATCH 263/295] feat: contract termination (#996) --- CHANGELOG.md | 4 +- README.md | 26 + docs/api/sovity-edc-api-wrapper.yaml | 19 +- docs/dev/intellij/intelliJ.md | 53 ++ .../catalog-crawler-e2e-test/build.gradle.kts | 3 +- .../ext/catalog/crawler/CrawlerE2eTest.java | 3 +- .../catalog-crawler/build.gradle.kts | 3 +- extensions/contract-termination/README.md | 39 ++ .../contract-termination/build.gradle.kts | 61 +++ .../ContractAgreementTerminationDetails.java | 47 ++ .../ContractAgreementTerminationService.java | 104 ++++ .../ContractTerminationExtension.java | 114 ++++ .../ContractTerminationMessage.java | 43 ++ .../ContractTerminationParam.java | 25 + .../TransferProcessBlocker.java | 40 ++ .../ContractAgreementIsTerminatedQuery.java | 33 ++ ...tractAgreementTerminationDetailsQuery.java | 53 ++ .../query/TerminateContractQuery.java | 50 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 14 + ...tAgreementTerminationDetailsQueryTest.java | 124 +++++ .../query/TerminateContractQueryTest.java | 83 +++ .../database-direct-access/build.gradle.kts | 25 + .../DatabaseDirectAccessExtension.java | 88 +++ .../db/directaccess/DslContextFactory.java | 58 ++ .../db/directaccess/RollbackException.java | 21 + ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../V10__add_contract_termination.sql | 25 + .../build.gradle.kts | 1 + extensions/sovity-messenger/build.gradle.kts | 3 +- .../extension/messenger/SovityMessage.java | 1 + .../extension/messenger/SovityMessenger.java | 5 + .../messenger/SovityMessengerException.java | 1 + .../messenger/SovityMessengerExtension.java | 10 +- .../messenger/SovityMessengerRegistry.java | 77 ++- .../controller/SovityMessageController.java | 15 +- .../impl/{Handler.java => HandlerBox.java} | 7 +- .../JsonObjectFromSovityMessageRequest.java | 1 + .../JsonObjectFromSovityMessageResponse.java | 1 + .../messenger/impl/MessageEmitter.java | 1 + .../messenger/impl/MessageReceiver.java | 1 + .../messenger/impl/ObjectMapperFactory.java | 1 + .../messenger/impl/SovityMessageRequest.java | 1 + .../messenger/impl/SovityMessageResponse.java | 1 + .../messenger/impl/SovityMessengerStatus.java | 1 + .../SovityMessengerExtensionE2eTest.java | 1 + .../messenger/controller/Answer.java | 1 + .../messenger/controller/Payload.java | 1 + .../SovityMessageControllerTest.java | 59 +- .../messenger/demo/SovityMessengerDemo.java | 13 + .../demo/SovityMessengerDemoTest.java | 95 ++-- .../messenger/demo/message/Addition.java | 1 + .../messenger/demo/message/Answer.java | 1 + .../messenger/demo/message/Counterparty.java | 24 + .../messenger/demo/message/Failing.java | 1 + .../demo/message/Multiplication.java | 1 + .../messenger/demo/message/Signal.java | 1 + .../messenger/demo/message/Sqrt.java | 1 + .../demo/message/UnregisteredMessage.java | 1 + .../edc/extension/messenger/dto/Addition.java | 1 + .../edc/extension/messenger/dto/Answer.java | 1 + .../messenger/dto/Multiplication.java | 1 + .../messenger/dto/UnsupportedMessage.java | 1 + .../echo/SovityMessageRequestTest.java | 1 + .../messenger/impl/MessageEmitterTest.java | 1 + .../impl/SovityMessengerRegistryImplTest.java | 4 +- .../messenger/impl/SovityMessengerTest.java | 4 +- .../TestBackendController.java | 3 +- .../TestBackendExtension.java | 3 +- .../client/examples/GreetingResourceTest.java | 3 - .../wrapper/wrapper-api/build.gradle.kts | 10 +- .../edc/ext/wrapper/api/ui/UiResource.java | 9 +- .../ui/model/ContractAgreementPageQuery.java | 4 + .../ContractAgreementTerminationInfo.java | 3 + .../api/ui/model/ContractTerminatedBy.java | 1 + .../ui/model/ContractTerminationRequest.java | 18 +- .../ui/model/ContractTerminationStatus.java | 1 + .../api/ui/model/UiCriterionOperator.java | 3 +- .../wrapper/api/usecase/UseCaseResource.java | 2 +- .../api/common/model/AtomicConstraintDto.java | 3 +- .../wrapper/api/common/model/OperatorDto.java | 3 +- .../api/common/model/PermissionDto.java | 3 +- .../wrapper-common-mappers/build.gradle.kts | 1 - .../api/common/mappers/PolicyMapper.java | 2 +- .../mappers/asset/AssetEditRequestMapper.java | 2 +- .../mappers/asset/AssetJsonLdBuilder.java | 3 +- .../mappers/asset/AssetJsonLdParser.java | 5 +- .../mappers/asset/utils/AssetJsonLdUtils.java | 3 +- .../mappers/asset/utils/EdcPropertyUtils.java | 5 +- .../asset/utils/FailedMappingException.java | 3 +- .../mappers/asset/utils/JsonBuilderUtils.java | 3 +- .../asset/utils/ShortDescriptionBuilder.java | 3 +- .../policy/AtomicConstraintMapper.java | 4 +- .../mappers/policy/ConstraintExtractor.java | 3 +- .../common/mappers/policy/LiteralMapper.java | 4 +- .../common/mappers/policy/MappingErrors.java | 3 +- .../mappers/policy/PolicyValidator.java | 4 +- .../asset/utils/EdcPropertyUtilsTest.java | 1 - .../utils/ShortDescriptionBuilderTest.java | 1 - .../policy/ConstraintExtractorTest.java | 4 - .../mappers/policy/LiteralMapperTest.java | 2 - .../mappers/policy/MappingErrorsTest.java | 1 - .../mappers/policy/OperatorMapperTest.java | 1 - .../mappers/policy/PolicyValidatorTest.java | 2 - extensions/wrapper/wrapper/build.gradle.kts | 23 +- .../edc/ext/wrapper/WrapperExtension.java | 52 +- .../WrapperExtensionContextBuilder.java | 246 +++++---- .../ext/wrapper/api/ui/UiResourceImpl.java | 25 +- .../ContractAgreementPageApiService.java | 27 +- ...ontractAgreementTerminationApiService.java | 53 ++ .../services/ContractAgreementData.java | 5 +- .../ContractAgreementDataFetcher.java | 47 +- .../ContractAgreementPageCardBuilder.java | 64 ++- .../policy/PolicyDefinitionApiService.java | 2 +- ...ransferHistoryPageAssetFetcherService.java | 3 +- .../api/usecase/UseCaseResourceImpl.java | 2 +- .../edc/ext/wrapper/utils/ValidatorUtils.java | 33 ++ .../de/sovity/edc/ext/wrapper/TestUtils.java | 85 --- .../ui/pages/asset/AssetApiServiceTest.java | 395 +++++++------- .../api/ui/pages/catalog/CatalogApiTest.java | 38 +- .../ContractAgreementPageTest.java | 160 +++--- ...ntractAgreementTransferApiServiceTest.java | 33 +- .../ContractDefinitionPageApiServiceTest.java | 41 +- .../DashboardPageApiServiceTest.java | 100 ++-- .../PolicyDefinitionApiServiceTest.java | 109 ++-- .../TransferHistoryPageApiServiceTest.java | 31 +- .../TransferProcessAssetApiServiceTest.java | 30 +- .../TransferProcessTestUtils.java | 5 +- .../ext/wrapper/api/usecase/KpiApiTest.java | 31 +- .../PolicyDefinitionApiServiceTest.java | 32 +- .../api/usecase/SupportedPolicyApiTest.java | 35 +- .../api/usecase/UseCaseApiWrapperTest.java | 47 +- gradle/libs.versions.toml | 13 +- settings.gradle.kts | 3 +- tests/build.gradle.kts | 10 +- .../de/sovity/edc/e2e/ApiWrapperDemoTest.java | 131 ++--- .../edc/e2e/ContractTerminationTest.java | 437 +++++++++++++++ .../e2e/DataSourceParameterizationTest.java | 502 +++++++++--------- .../edc/e2e/DataSourceQueryParamsTest.java | 180 +------ .../edc/e2e/ManagementApiTransferTest.java | 51 +- .../edc/e2e/Ms8ConnectorMigrationTest.java | 231 -------- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 126 ++--- .../sovity/edc/e2e/UseCaseApiWrapperTest.java | 81 +-- utils/catalog-parser/build.gradle.kts | 1 - .../edc/utils/catalog/DspCatalogService.java | 3 +- .../catalog/DspCatalogServiceException.java | 3 +- .../catalog/mapper/DspContractOfferUtils.java | 78 --- .../catalog/mapper/DspDataOfferBuilder.java | 3 +- .../edc/utils/catalog/model/DspCatalog.java | 3 +- .../utils/catalog/model/DspContractOffer.java | 3 +- .../edc/utils/catalog/model/DspDataOffer.java | 3 +- utils/json-and-jsonld-utils/build.gradle.kts | 1 - .../java/de/sovity/edc/utils/JsonUtils.java | 3 +- .../sovity/edc/utils/jsonld/JsonLdUtils.java | 3 +- .../sovity/edc/utils/jsonld/vocab/Prop.java | 3 +- utils/test-connector-remote/build.gradle.kts | 35 -- .../EdcRuntimeExtensionWithTestDatabase.java | 43 -- .../README.md | 6 +- utils/test-utils/build.gradle.kts | 22 + .../e2e/connector/ConnectorRemote.java | 22 +- .../e2e/connector/DataTransferTestUtil.java | 3 +- .../e2e/connector/MockDataAddressRemote.java | 3 +- .../e2e/connector/config/ConnectorConfig.java | 3 +- .../config/ConnectorConfigFactory.java | 38 +- .../config/ConnectorRemoteConfig.java | 3 +- .../config/ConnectorRemoteConfigFactory.java | 3 +- .../config/DatasourceConfigUtils.java | 3 +- .../connector/config/api/EdcApiConfig.java | 3 +- .../config/api/EdcApiConfigFactory.java | 3 +- .../e2e/connector/config/api/EdcApiGroup.java | 3 +- .../config/api/EdcApiGroupConfig.java | 3 +- .../config/api/auth/ApiKeyAuthProvider.java | 3 +- .../config/api/auth/AuthProvider.java | 3 +- .../config/api/auth/NoneAuthProvider.java | 3 +- .../e2e/db/EdcRuntimeExtensionDeferred.java | 0 .../e2e/db/EdcRuntimeExtensionFixed.java | 0 .../EdcRuntimeExtensionWithTestDatabase.java | 69 +++ .../edc/extension/e2e/db/JdbcCredentials.java | 0 .../edc/extension/e2e/db/TestDatabase.java | 0 .../extension/e2e/db/TestDatabaseFactory.java | 0 .../e2e/db/TestDatabaseViaEnvVars.java | 0 .../e2e/db/TestDatabaseViaTestcontainers.java | 0 .../sovity/edc/extension/e2e/env/EnvUtil.java | 3 +- .../edc/extension/e2e/extension/Consumer.java | 25 + .../extension/e2e/extension/E2eScenario.java | 275 ++++++++++ .../e2e/extension/E2eTestExtension.java | 207 ++++++++ .../extension/e2e/extension/MockedAsset.java | 29 + .../edc/extension/e2e/extension/Provider.java | 25 + .../de/sovity/edc/extension/utils/Lazy.java | 38 ++ .../utils/junit/DisabledOnGithub.java | 1 + 189 files changed, 4158 insertions(+), 1976 deletions(-) create mode 100644 docs/dev/intellij/intelliJ.md create mode 100644 extensions/contract-termination/README.md create mode 100644 extensions/contract-termination/build.gradle.kts create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationDetails.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationMessage.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationParam.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/TransferProcessBlocker.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementIsTerminatedQuery.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQuery.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java create mode 100644 extensions/contract-termination/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java create mode 100644 extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java create mode 100644 extensions/database-direct-access/build.gradle.kts create mode 100644 extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java create mode 100644 extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DslContextFactory.java create mode 100644 extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/RollbackException.java create mode 100644 extensions/database-direct-access/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V10__add_contract_termination.sql rename extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/{Handler.java => HandlerBox.java} (69%) create mode 100644 extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Counterparty.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTerminationApiService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/ValidatorUtils.java delete mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java delete mode 100644 tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java delete mode 100644 utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java delete mode 100644 utils/test-connector-remote/build.gradle.kts delete mode 100644 utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java rename utils/{test-connector-remote => test-utils}/README.md (75%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java (95%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java (98%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java (98%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java (97%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java (75%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java (96%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java (99%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java (98%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java (97%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java (99%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java (97%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java (96%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java (95%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java (93%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java (95%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java (100%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java (100%) create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java (100%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java (100%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java (100%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java (100%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java (100%) rename utils/{test-connector-remote => test-utils}/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java (97%) create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Consumer.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/MockedAsset.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Provider.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/utils/Lazy.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c677656c..244b9dc34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Both providers and consumers can now terminate their contracts. + #### Patch Changes ### Deployment Migration Notes @@ -548,7 +550,7 @@ https://github.com/sovity/edc-ui/releases/tag/v2.0.0 - New modules with common UI models and mappers for the Connector UI and Broker UI: `:extensions:wrapper:wrapper-common-api` and `:extensions:wrapper:wrapper-common-mappers`. - New module with centralized Vocab and utilities for dealing with EDC / DCAT JSON-LD: `:utils:json-and-jsonld-utils` - New module with utilities for parsing DCAT Catalog responses for use in the UI API Wrapper and the Broker Server: `:utils:catalog-parser` -- New modules with utilities for E2E Testing Connectors: `:utils:test-connector-remote` and `:extensions:test-backend-controller` +- New modules with utilities for E2E Testing Connectors: `:utils:test-utils` and `:extensions:test-backend-controller` #### Patch Changes diff --git a/README.md b/README.md index 7acfb1783..09d3d64ba 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,32 @@ Our contribution guideline can be found in [CONTRIBUTING.md](CONTRIBUTING.md).

      (back to top)

      +## Development + +### Developer Documentation + +- Local Development Guide (further below) +- [IntelliJ](docs/dev/intellij/intelliJ.md) +- [Changelog Updates](docs/dev/changelog_updates.md) + +

      (back to top)

      + +### Requirements + +- Docker Environment +- JDK 17 +- GitHub Maven Registry Access + +To access the GitHub Maven Registry you need to provide the following properties, e.g. by providing +a `~/.gradle/gradle.properties`. + +```properties +gpr.user={your github username} +gpr.key={your github pat with packages.read} +``` + +

      (back to top)

      + ## License diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 13d5135d1..851967da8 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -255,8 +255,9 @@ paths: description: Collect filtered data for the Contract Agreement Page operationId: getContractAgreementPage requestBody: + description: "If null, returns all the contract agreements." content: - '*/*': + application/json: schema: $ref: '#/components/schemas/ContractAgreementPageQuery' responses: @@ -524,11 +525,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource - default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM + default: CUSTOM SecretValue: type: object properties: @@ -736,7 +737,6 @@ components: UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource - default: GET enum: - GET - POST @@ -744,6 +744,7 @@ components: - PATCH - DELETE - OPTIONS + default: GET UiDataSourceOnRequest: required: - contactEmail @@ -1788,14 +1789,18 @@ components: - reason type: object properties: - reason: - title: Termination reason - type: string - description: A short reason why this contract was terminated detail: title: Termination detail + maxLength: 1000 + minLength: 0 type: string description: A user explanation to detail why the contract was terminated. + reason: + title: Termination reason + maxLength: 100 + minLength: 0 + type: string + description: A short reason why this contract was terminated description: Data for terminating a Contract Agreement AtomicConstraintDto: required: diff --git a/docs/dev/intellij/intelliJ.md b/docs/dev/intellij/intelliJ.md new file mode 100644 index 000000000..0f30676ec --- /dev/null +++ b/docs/dev/intellij/intelliJ.md @@ -0,0 +1,53 @@ +# IntelliJ + +## Checkstyle + +Checkstyle errors are by default not shown in IntelliJ. + +There is an [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) to see Checkstyle errors as errors in your IDE. + +### License Headers with IntelliJ + +While it's close to impossible to write a regex to validate all the possible copyright messages, the one currently configured in [checkstyle-config.xml](../checkstyle/checkstyle-config.xml) matches almost all the files in this project. + +Failing to use this template will make it progressively harder to fix the checkstyle errors as the copyright warnings will accumulate and dilute those errors, wasting precious brain time. + +IntelliJ has a feature to help maintain consistent copyright headers in + +`Settings > Editor > Copyright > Copyright profiles`. + +The copyright below passes the checkstyle when put in a Java multiline comment. + +``` +Copyright (c) 2024 sovity GmbH + + This program and the accompanying materials are made available under the + terms of the Apache License, Version 2.0 which is available at + https://www.apache.org/licenses/LICENSE-2.0 + + SPDX-License-Identifier: Apache-2.0 + + Contributors: + sovity GmbH - initial API and implementation + +``` + +Which once inserted at the top of a file will look like + +```java +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package foo.bar; +``` diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts b/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts index 6cc40c67d..9f3492139 100644 --- a/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/build.gradle.kts @@ -10,7 +10,7 @@ dependencies { testCompileOnly(libs.lombok) testImplementation(project(":utils:versions")) - testImplementation(project(":utils:test-connector-remote")) + testImplementation(project(":utils:test-utils")) testImplementation(project(":utils:json-and-jsonld-utils")) testImplementation(project(":extensions:catalog-crawler:catalog-crawler-db")) testImplementation(project(":extensions:wrapper:clients:java-client")) @@ -18,7 +18,6 @@ dependencies { testImplementation(libs.assertj.core) testImplementation(libs.mockito.core) - testImplementation(libs.mockito.inline) testImplementation(libs.edc.junit) testImplementation(libs.restAssured.restAssured) testImplementation(libs.testcontainers.testcontainers) diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java index 3fedd0961..db8a7631e 100644 --- a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.catalog.crawler; diff --git a/extensions/catalog-crawler/catalog-crawler/build.gradle.kts b/extensions/catalog-crawler/catalog-crawler/build.gradle.kts index b63b7004f..7c2b46f7e 100644 --- a/extensions/catalog-crawler/catalog-crawler/build.gradle.kts +++ b/extensions/catalog-crawler/catalog-crawler/build.gradle.kts @@ -21,10 +21,9 @@ dependencies { testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) - testImplementation(project(":utils:test-connector-remote")) + testImplementation(project(":utils:test-utils")) testImplementation(libs.assertj.core) testImplementation(libs.mockito.core) - testImplementation(libs.mockito.inline) testImplementation(libs.restAssured.restAssured) testImplementation(libs.testcontainers.testcontainers) testImplementation(libs.flyway.core) diff --git a/extensions/contract-termination/README.md b/extensions/contract-termination/README.md new file mode 100644 index 000000000..e9dbda30f --- /dev/null +++ b/extensions/contract-termination/README.md @@ -0,0 +1,39 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      Contract Termination

      + +

      + Report Bug + · + Request Feature +

      +
      + + +## About this Extension + +Using our [`sovity-messenger`](../sovity-messenger) extension, both providers and consumers can now terminate contracts in a transparent way. Contract termination information is persisted in its own table on both sides and terminated contracts will be prevented from being transferable. + +## Why does this extension exist? + +Contracts termination is not natively supported by the Data Space Protocol (DSP) or in the Eclipse EDC. Customer Feedback and real-world Dataspace projects have proven for there to be many reasons for a party to want to terminate a contract. We want to enable contract termination in a transparent way for all participating parties, the provider, the consumer, and if necessary, the authority via the Logging House. + +## Details + +When a User clicks "Terminate contract" on a contract agreement, a request is sent to the EDC to mark the contract agreement as terminated, followed by a notification and registration of that same termination on the counterpart's side. + +The termination is saved in the EDC's database. +Any transfer started from this contract agreement will be rejected. + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/contract-termination/build.gradle.kts b/extensions/contract-termination/build.gradle.kts new file mode 100644 index 000000000..61c1ad4ec --- /dev/null +++ b/extensions/contract-termination/build.gradle.kts @@ -0,0 +1,61 @@ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + implementation(project(":utils:jooq-database-access")) + implementation(project(":extensions:database-direct-access")) + implementation(project(":extensions:sovity-messenger")) + + implementation(libs.edc.coreSpi) + implementation(libs.edc.transferSpi) + implementation(libs.edc.dspNegotiationTransform) + + implementation(libs.jakarta.rsApi) + + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + + testImplementation(project(":extensions:postgres-flyway")) + testImplementation(project(":utils:test-utils")) + testImplementation(project(":utils:versions")) + + testImplementation(libs.edc.http) { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation(libs.bundles.jetty.cve2023) + + testImplementation(libs.assertj.core) + testImplementation(libs.flyway.core) + testImplementation(libs.junit.api) + testImplementation(libs.hibernate.validation) + testImplementation(libs.jakarta.el) + testImplementation(libs.mockito.core) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.testcontainers.postgresql) + + testRuntimeOnly(libs.junit.engine) +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationDetails.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationDetails.java new file mode 100644 index 000000000..a16096979 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationDetails.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy; +import lombok.Builder; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; + +import java.time.OffsetDateTime; + +@Builder(toBuilder = true) +public record ContractAgreementTerminationDetails( + String contractAgreementId, + String counterpartyId, + String counterpartyAddress, + ContractNegotiation.Type type, + String providerAgentId, + String consumerAgentId, + String reason, + String detail, + OffsetDateTime terminatedAt, + ContractTerminatedBy terminatedBy +) { + public boolean isTerminated() { + return terminatedAt != null; + } + + boolean thisEdcIsTheConsumer() { + return type.equals(ContractNegotiation.Type.CONSUMER); + } + + boolean thisEdcIsTheProvider() { + return type.equals(ContractNegotiation.Type.PROVIDER); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java new file mode 100644 index 000000000..36a151b5b --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import de.sovity.edc.extension.contacttermination.query.ContractAgreementTerminationDetailsQuery; +import de.sovity.edc.extension.contacttermination.query.TerminateContractQuery; +import de.sovity.edc.extension.messenger.SovityMessenger; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.Nullable; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; + +import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.COUNTERPARTY; +import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.SELF; + +@RequiredArgsConstructor +public class ContractAgreementTerminationService { + + private final SovityMessenger sovityMessenger; + private final ContractAgreementTerminationDetailsQuery contractAgreementTerminationDetailsQuery; + private final TerminateContractQuery terminateContractQuery; + private final Monitor monitor; + private final String thisParticipantId; + + /** + * This is to terminate an EDC's own contract. + * If the termination comes from an external system, use + * {@link #terminateCounterpartyAgreement(DSLContext, String, ContractTerminationParam)} + * to validate the counter-party's identity. + */ + public OffsetDateTime terminateAgreementOrThrow(DSLContext dsl, ContractTerminationParam termination) { + + val details = contractAgreementTerminationDetailsQuery.fetchAgreementDetailsOrThrow(dsl, termination.contractAgreementId()); + + if (details == null) { + throw new EdcException("Could not find the contract agreement with ID %s.".formatted(termination.contractAgreementId())); + } + + if (details.isTerminated()) { + return details.terminatedAt(); + } + + val terminatedAt = terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, SELF); + + notifyTerminationToProvider(details.counterpartyAddress(), termination); + + return terminatedAt; + } + + public OffsetDateTime terminateCounterpartyAgreement( + DSLContext dsl, + @Nullable String identity, + ContractTerminationParam termination + ) { + val details = contractAgreementTerminationDetailsQuery.fetchAgreementDetailsOrThrow(dsl, termination.contractAgreementId()); + + if (details == null) { + val message = "Could not find the contract agreement with ID %s.".formatted(termination.contractAgreementId()); + throw new EdcException(message); + } + + boolean thisEdcIsConsumerAndSenderIsProvider = details.thisEdcIsTheConsumer() && details.providerAgentId().equals(identity); + boolean thisEdcIsProviderAndSenderIsConsumer = details.thisEdcIsTheProvider() && details.consumerAgentId().equals(identity); + + if (!(thisEdcIsConsumerAndSenderIsProvider || thisEdcIsProviderAndSenderIsConsumer)) { + monitor.severe( + "The EDC %s attempted to terminate a contract that it was not related to!".formatted(details.consumerAgentId())); + throw new EdcException("The requester's identity %s is neither the consumer nor the provider".formatted(identity)); + } + + if (details.isTerminated()) { + throw new EdcException("The contract is already terminated"); + } + + val agent = thisParticipantId.equals(details.counterpartyId()) ? SELF : COUNTERPARTY; + + return terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, agent); + } + + public void notifyTerminationToProvider(String counterPartyAddress, ContractTerminationParam termination) { + sovityMessenger.send( + counterPartyAddress, + new ContractTerminationMessage( + termination.contractAgreementId(), + termination.detail(), + termination.reason())); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java new file mode 100644 index 000000000..cad805be9 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import de.sovity.edc.extension.contacttermination.query.ContractAgreementIsTerminatedQuery; +import de.sovity.edc.extension.contacttermination.query.ContractAgreementTerminationDetailsQuery; +import de.sovity.edc.extension.contacttermination.query.TerminateContractQuery; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import lombok.val; +import org.eclipse.edc.connector.transfer.spi.observe.TransferProcessObservable; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.agent.ParticipantAgentService; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + + +@Provides(ContractAgreementTerminationService.class) +public class ContractTerminationExtension implements ServiceExtension { + + @Setting(required = true) + private static final String EDC_PARTICIPANT_ID = "edc.participant.id"; + + @Inject + private DslContextFactory dslContextFactory; + + @Inject + private IdentityService identityService; + + @Inject + private Monitor monitor; + + @Inject + private SovityMessenger sovityMessenger; + + @Inject + private SovityMessengerRegistry messengerRegistry; + + @Inject + private TransferProcessObservable observable; + + @Inject + private ParticipantAgentService participantAgentService; + + @Override + public void initialize(ServiceExtensionContext context) { + + val terminationService = setupTerminationService(context); + setupMessenger(terminationService); + setupTransferPrevention(); + } + + private ContractAgreementTerminationService setupTerminationService(ServiceExtensionContext context) { + + val config = context.getConfig(); + val contractAgreementTerminationDetailsQuery = new ContractAgreementTerminationDetailsQuery(); + val terminateContractQuery = new TerminateContractQuery(); + + val terminationService = new ContractAgreementTerminationService( + sovityMessenger, + contractAgreementTerminationDetailsQuery, + terminateContractQuery, + monitor, + config.getString(EDC_PARTICIPANT_ID) + ); + + context.registerService(ContractAgreementTerminationService.class, terminationService); + + return terminationService; + } + + private void setupMessenger(ContractAgreementTerminationService terminationService) { + messengerRegistry.register( + ContractTerminationMessage.class, + (claims, termination) -> + dslContextFactory.transactionResult(dsl -> + terminationService.terminateCounterpartyAgreement( + dsl, + participantAgentService.createFor(claims).getIdentity(), + buildTerminationRequest(termination)))); + } + + private static ContractTerminationParam buildTerminationRequest(ContractTerminationMessage message) { + return new ContractTerminationParam( + message.getContractAgreementId(), + message.getDetail(), + message.getReason() + ); + } + + private void setupTransferPrevention() { + observable.registerListener( + new TransferProcessBlocker( + dslContextFactory, + new ContractAgreementIsTerminatedQuery())); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationMessage.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationMessage.java new file mode 100644 index 000000000..dbf3c4da7 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationMessage.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@Getter +@NoArgsConstructor +public class ContractTerminationMessage implements SovityMessage { + + @JsonProperty("contractAgreementId") + private String contractAgreementId; + + @JsonProperty("detail") + private String detail; + + @JsonProperty("reason") + private String reason; + + @Override + public String getType() { + return "io.sovity.message.contract.terminate"; + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationParam.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationParam.java new file mode 100644 index 000000000..995830d66 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationParam.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import lombok.Builder; + +@Builder +public record ContractTerminationParam( + String contractAgreementId, + String detail, + String reason +) { +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/TransferProcessBlocker.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/TransferProcessBlocker.java new file mode 100644 index 000000000..256b8f179 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/TransferProcessBlocker.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import de.sovity.edc.extension.contacttermination.query.ContractAgreementIsTerminatedQuery; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.connector.transfer.spi.observe.TransferProcessListener; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; + +@RequiredArgsConstructor +public class TransferProcessBlocker implements TransferProcessListener { + + private final DslContextFactory dslContextFactory; + private final ContractAgreementIsTerminatedQuery contractAgreementIsTerminated; + + @Override + public void preRequesting(TransferProcess process) { + val terminated = dslContextFactory.transactionResult(dsl -> + contractAgreementIsTerminated.isTerminated(dsl, process.getContractId())); + + if (terminated) { + val message = "Interrupted: the contract agreement %s is terminated.".formatted(process.getContractId()); + throw new IllegalStateException(message); + } + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementIsTerminatedQuery.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementIsTerminatedQuery.java new file mode 100644 index 000000000..05b31ba3c --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementIsTerminatedQuery.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination.query; + +import de.sovity.edc.ext.db.jooq.Tables; +import lombok.val; +import org.jooq.DSLContext; + +public class ContractAgreementIsTerminatedQuery { + + public boolean isTerminated(DSLContext dsl, String contractAgreementId) { + + val t = Tables.SOVITY_CONTRACT_TERMINATION; + + return dsl.fetchExists( + dsl.select() + .from(t) + .where(t.CONTRACT_AGREEMENT_ID.eq(contractAgreementId)) + ); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQuery.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQuery.java new file mode 100644 index 000000000..46454d025 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination.query; + +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationDetails; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.jooq.DSLContext; + +import static de.sovity.edc.ext.db.jooq.Tables.EDC_CONTRACT_AGREEMENT; +import static de.sovity.edc.ext.db.jooq.Tables.EDC_CONTRACT_NEGOTIATION; +import static de.sovity.edc.ext.db.jooq.Tables.SOVITY_CONTRACT_TERMINATION; + +@RequiredArgsConstructor +public class ContractAgreementTerminationDetailsQuery { + + public ContractAgreementTerminationDetails fetchAgreementDetailsOrThrow(DSLContext dsl, String agreementId) { + val n = EDC_CONTRACT_NEGOTIATION; + val a = EDC_CONTRACT_AGREEMENT; + val t = SOVITY_CONTRACT_TERMINATION; + + return dsl.transactionResult(trx -> + trx.dsl().select( + n.AGREEMENT_ID, + n.COUNTERPARTY_ID, + n.COUNTERPARTY_ADDRESS, + n.TYPE.convertFrom(ContractNegotiation.Type::valueOf), + a.PROVIDER_AGENT_ID, + a.CONSUMER_AGENT_ID, + t.REASON, + t.DETAIL, + t.TERMINATED_AT, + t.TERMINATED_BY) + .from( + n.join(a).on(n.AGREEMENT_ID.eq(a.AGR_ID)) + .leftJoin(SOVITY_CONTRACT_TERMINATION).on(n.AGREEMENT_ID.eq(t.CONTRACT_AGREEMENT_ID))) + .where(a.AGR_ID.eq(agreementId)) + .fetchOneInto(ContractAgreementTerminationDetails.class)); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java new file mode 100644 index 000000000..f404eb92c --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination.query; + +import de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy; +import de.sovity.edc.extension.contacttermination.ContractTerminationParam; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.jooq.DSLContext; + +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; + +import static de.sovity.edc.ext.db.jooq.Tables.SOVITY_CONTRACT_TERMINATION; + +@RequiredArgsConstructor +public class TerminateContractQuery { + + public OffsetDateTime terminateConsumerAgreementOrThrow( + DSLContext dsl, + ContractTerminationParam termination, + ContractTerminatedBy terminatedBy) { + + val tooAccurate = OffsetDateTime.now(); + val now = tooAccurate.truncatedTo(ChronoUnit.MICROS); + + val newTermination = dsl.newRecord(SOVITY_CONTRACT_TERMINATION); + newTermination.setContractAgreementId(termination.contractAgreementId()); + newTermination.setDetail(termination.detail()); + newTermination.setReason(termination.reason()); + newTermination.setTerminatedBy(terminatedBy); + newTermination.setTerminatedAt(now); + + newTermination.insert(); + + return now; + } +} diff --git a/extensions/contract-termination/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/contract-termination/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..61b77a354 --- /dev/null +++ b/extensions/contract-termination/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,14 @@ +# +# Copyright (c) 2024 sovity GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# sovity GmbH - initial API and implementation +# + +de.sovity.edc.extension.contacttermination.ContractTerminationExtension diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java new file mode 100644 index 000000000..a13d156c9 --- /dev/null +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination.query; + +import de.sovity.edc.client.gen.model.ContractTerminationRequest; +import de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationDetails; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; + + +@ExtendWith(E2eTestExtension.class) +class ContractAgreementTerminationDetailsQueryTest { + + @Test + void fetchAgreementDetailsOrThrow_whenAgreementIsPresent_shouldReturnTheAgreementDetails( + E2eScenario scenario, + @Consumer DslContextFactory dslContextFactory, + @Provider ConnectorConfig providerConfig + ) { + // arrange + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiations = scenario.negotiateAssetAndAwait(assetId); + + dslContextFactory.rollbackTransaction(dsl -> { + val query = new ContractAgreementTerminationDetailsQuery(); + + // act + val agreementId = negotiations.getContractAgreementId(); + val details = query.fetchAgreementDetailsOrThrow(dsl, agreementId); + + // assert + assertThat(details).isEqualTo(ContractAgreementTerminationDetails.builder() + .contractAgreementId(agreementId) + .counterpartyId("provider") + .counterpartyAddress(providerConfig.getProtocolEndpoint().getUri().toString()) + .type(CONSUMER) + .providerAgentId("provider") + .consumerAgentId("consumer") + .reason(null) + .detail(null) + .terminatedAt(null) + .terminatedBy(null) + .build()); + } + ); + } + + @Test + void fetchAgreementDetailsOrThrow_whenAgreementIsMissing_shouldReturnEmptyOptional(@Consumer DslContextFactory dslContextFactory) { + // arrange + + dslContextFactory.rollbackTransaction(dsl -> { + val query = new ContractAgreementTerminationDetailsQuery(); + + // act + val details = query.fetchAgreementDetailsOrThrow(dsl, "agreement:doesnt:exist"); + + // assert + assertThat(details).isNull(); + } + ); + } + + @Test + void fetchAgreementDetailsOrThrow_whenTerminationAlreadyExists_shouldReturnOptionalWithTerminationData( + E2eScenario scenario, + @Consumer DslContextFactory dslContextFactory, + @Provider ConnectorConfig providerConfig + ) { + // arrange + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiations = scenario.negotiateAssetAndAwait(assetId); + val terminationRequest = new ContractTerminationRequest("Terminated because of good reasons", "User Termination"); + val termination = scenario.terminateContractAgreementAndAwait(CONSUMER, negotiations.getContractAgreementId(), terminationRequest); + + dslContextFactory.rollbackTransaction(dsl -> { + val query = new ContractAgreementTerminationDetailsQuery(); + + // act + val agreementId = negotiations.getContractAgreementId(); + val details = query.fetchAgreementDetailsOrThrow(dsl, agreementId); + + // assert + assertThat(details.contractAgreementId()).isEqualTo(agreementId); + assertThat(details.counterpartyId()).isEqualTo("provider"); + assertThat(details.counterpartyAddress()).isEqualTo(providerConfig.getProtocolEndpoint().getUri().toString()); + assertThat(details.type()).isEqualTo(CONSUMER); + assertThat(details.providerAgentId()).isEqualTo("provider"); + assertThat(details.consumerAgentId()).isEqualTo("consumer"); + assertThat(details.reason()).isEqualTo("User Termination"); + assertThat(details.detail()).isEqualTo("Terminated because of good reasons"); + assertThat(details.terminatedAt()).isEqualTo(termination.getLastUpdatedDate()); + assertThat(details.terminatedBy()).isEqualTo(ContractTerminatedBy.SELF); + } + ); + } +} diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java new file mode 100644 index 000000000..c4e5dc825 --- /dev/null +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination.query; + +import de.sovity.edc.extension.contacttermination.ContractTerminationParam; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import lombok.val; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.OffsetDateTime; + +import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.COUNTERPARTY; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(E2eTestExtension.class) +class TerminateContractQueryTest { + + @Test + void terminateConsumerAgreementOrThrow_shouldInsertRowInTerminationTable( + E2eScenario scenario, + @Consumer DslContextFactory dslContextFactory, + @Provider ConnectorConfig providerConfig + ) { + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + dslContextFactory.rollbackTransaction(trx -> { + + // arrange + val query = new TerminateContractQuery(); + val agreementId = negotiation.getContractAgreementId(); + + val details = new ContractTerminationParam( + agreementId, + "Some detail", + "Some reason" + ); + val now = OffsetDateTime.now(); + + // act + val terminatedAt = query.terminateConsumerAgreementOrThrow(trx.dsl(), details, COUNTERPARTY); + + // assert + assertThat(terminatedAt).isNotNull(); + + val detailsQuery = new ContractAgreementTerminationDetailsQuery(); + val detailsAfterTermination = detailsQuery.fetchAgreementDetailsOrThrow(trx.dsl(), agreementId); + + assertThat(detailsAfterTermination.contractAgreementId()).isEqualTo(agreementId); + assertThat(detailsAfterTermination.counterpartyId()).isEqualTo("provider"); + assertThat(detailsAfterTermination.counterpartyAddress()) + .isEqualTo(providerConfig.getProtocolEndpoint().getUri().toString()); + assertThat(detailsAfterTermination.type()).isEqualTo(ContractNegotiation.Type.CONSUMER); + assertThat(detailsAfterTermination.providerAgentId()).isEqualTo("provider"); + assertThat(detailsAfterTermination.consumerAgentId()).isEqualTo("consumer"); + assertThat(detailsAfterTermination.reason()).isEqualTo("Some reason"); + assertThat(detailsAfterTermination.detail()).isEqualTo("Some detail"); + assertThat(detailsAfterTermination.terminatedAt()).isBetween(now, now.plusSeconds(1)); + assertThat(detailsAfterTermination.terminatedBy()).isEqualTo(COUNTERPARTY); + } + ); + } +} diff --git a/extensions/database-direct-access/build.gradle.kts b/extensions/database-direct-access/build.gradle.kts new file mode 100644 index 000000000..3aecfb4d1 --- /dev/null +++ b/extensions/database-direct-access/build.gradle.kts @@ -0,0 +1,25 @@ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + implementation(libs.edc.coreSpi) + + implementation(libs.jooq.jooq) + implementation(libs.hikari) +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java new file mode 100644 index 000000000..a69d4eba8 --- /dev/null +++ b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.db.directaccess; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.val; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +@Provides(DslContextFactory.class) +public class DatabaseDirectAccessExtension implements ServiceExtension { + public static final String NAME = "DirectDatabaseAccess"; + + /** + * The JDBC URL. + */ + @Setting(required = true) + public static final String EDC_DATASOURCE_DEFAULT_URL = "edc.datasource.default.url"; + + /** + * The JDBC user. + */ + @Setting(required = true) + public static final String EDC_DATASOURCE_JDBC_USER = "edc.datasource.default.user"; + + /** + * The JDBC password. + */ + @Setting(required = true) + public static final String EDC_DATASOURCE_JDBC_PASSWORD = "edc.datasource.default.password"; + + /** + * Sets the connection pool size to use during the flyway migration. + */ + @Setting(defaultValue = "3") + public static final String EDC_SERVER_DB_CONNECTION_POOL_SIZE = "edc.server.db.connection.pool.size"; + + /** + * Sets the connection timeout for the datasource in milliseconds. + */ + @Setting(defaultValue = "5000") + public static final String EDC_SERVER_DB_CONNECTION_TIMEOUT_IN_MS = "edc.server.db.connection.timeout.in.ms"; + + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + initializeDirectDatabaseAccess(context); + } + + private void initializeDirectDatabaseAccess(ServiceExtensionContext context) { + + val hikariConfig = new HikariConfig(); + val config = context.getConfig(); + hikariConfig.setJdbcUrl(config.getString(EDC_DATASOURCE_DEFAULT_URL)); + hikariConfig.setUsername(config.getString(EDC_DATASOURCE_JDBC_USER)); + hikariConfig.setPassword(config.getString(EDC_DATASOURCE_JDBC_PASSWORD)); + hikariConfig.setMinimumIdle(1); + hikariConfig.setMaximumPoolSize(config.getInteger(EDC_SERVER_DB_CONNECTION_POOL_SIZE)); + hikariConfig.setIdleTimeout(30000); + hikariConfig.setPoolName("direct-database-access"); + hikariConfig.setMaxLifetime(50000); + hikariConfig.setConnectionTimeout(config.getInteger(EDC_SERVER_DB_CONNECTION_TIMEOUT_IN_MS)); + + val dda = new DslContextFactory(new HikariDataSource(hikariConfig)); + + context.registerService(DslContextFactory.class, dda); + } +} diff --git a/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DslContextFactory.java b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DslContextFactory.java new file mode 100644 index 000000000..dbca8cca0 --- /dev/null +++ b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DslContextFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.db.directaccess; + +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; + +import java.util.function.Consumer; +import java.util.function.Function; +import javax.sql.DataSource; + +@ExtensionPoint +@RequiredArgsConstructor +public class DslContextFactory { + + private final DataSource dataSource; + + private DSLContext newDslContext() { + return DSL.using(dataSource, SQLDialect.POSTGRES); + } + + public void transaction(Consumer consumer) { + newDslContext().transaction((trx) -> consumer.accept(trx.dsl())); + } + + /** + * For test purposes: a transaction that never succeeds + */ + public void rollbackTransaction(Consumer consumer) { + try { + newDslContext().transaction((trx) -> { + consumer.accept(trx.dsl()); + throw new RollbackException(); + }); + } catch (RollbackException e) { + // swallowed. Expected. + } + } + + public T transactionResult(Function f) { + return newDslContext().transactionResult((trx) -> f.apply(trx.dsl())); + } +} diff --git a/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/RollbackException.java b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/RollbackException.java new file mode 100644 index 000000000..2a75236aa --- /dev/null +++ b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/RollbackException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.db.directaccess; + +public class RollbackException extends RuntimeException { + public RollbackException() { + super("Rolled back."); + } +} diff --git a/extensions/database-direct-access/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/database-direct-access/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..2241735a9 --- /dev/null +++ b/extensions/database-direct-access/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.db.directaccess.DatabaseDirectAccessExtension diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V10__add_contract_termination.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V10__add_contract_termination.sql new file mode 100644 index 000000000..24c8c3d88 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V10__add_contract_termination.sql @@ -0,0 +1,25 @@ +-- +-- Copyright (c) 2024 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - initial API and implementation +-- + +create type contract_terminated_by as enum ('SELF', 'COUNTERPARTY'); + +create table sovity_contract_termination +( + contract_agreement_id varchar primary key, + reason text not null, + detail text not null, + terminated_at timestamp with time zone not null, + terminated_by contract_terminated_by not null, + constraint agreement_fk foreign key (contract_agreement_id) + references edc_contract_agreement (agr_id) +); diff --git a/extensions/sovity-edc-extensions-package/build.gradle.kts b/extensions/sovity-edc-extensions-package/build.gradle.kts index 10a522670..dc4edc6d1 100644 --- a/extensions/sovity-edc-extensions-package/build.gradle.kts +++ b/extensions/sovity-edc-extensions-package/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { api(project(":extensions:policy-always-true")) // API Extensions + api(project(":extensions:contract-termination")) api(project(":extensions:edc-ui-config")) api(project(":extensions:last-commit-info")) api(project(":extensions:wrapper:wrapper")) diff --git a/extensions/sovity-messenger/build.gradle.kts b/extensions/sovity-messenger/build.gradle.kts index 815865804..cd9c7655d 100644 --- a/extensions/sovity-messenger/build.gradle.kts +++ b/extensions/sovity-messenger/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { testCompileOnly(libs.lombok) - testImplementation(project(":utils:test-connector-remote")) testImplementation(project(":utils:test-utils")) testImplementation(libs.edc.junit) @@ -48,8 +47,8 @@ dependencies { testImplementation(libs.junit.api) testImplementation(libs.jsonAssert) testImplementation(libs.mockito.core) - testImplementation(libs.mockito.inline) testImplementation(libs.mockserver.netty) + testImplementation(libs.postgres) testImplementation(libs.restAssured.restAssured) testImplementation(libs.testcontainers.testcontainers) testImplementation(libs.testcontainers.junitJupiter) diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java index 04d07f0a1..1c049f0d6 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java index 82dbfe54d..38791ddfa 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger; @@ -23,6 +24,7 @@ import lombok.val; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.response.StatusResult; import org.jetbrains.annotations.NotNull; @@ -42,6 +44,8 @@ public class SovityMessenger { private final ObjectMapper serializer; + private final Monitor monitor; + /** * Sends a message to the counterparty address and returns a future result. * @@ -94,6 +98,7 @@ private Function header.getString(SovityMessengerStatus.HANDLER_EXCEPTION.getCode(), "No outgoing body."), payload); } else { + monitor.info(header.getString("message", "No message in the response header.")); throw new SovityMessengerException(header.getString("message")); } } catch (JsonProcessingException e) { diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java index 2de888e30..dd3b9290b 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java index 5a01dc7c0..545529043 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java @@ -9,11 +9,13 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.sovity.edc.extension.messenger.controller.SovityMessageController; import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageRequest; import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageResponse; @@ -27,6 +29,7 @@ import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.spi.agent.ParticipantAgentService; import org.eclipse.edc.spi.iam.IdentityService; import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.monitor.Monitor; @@ -68,6 +71,9 @@ public class SovityMessengerExtension implements ServiceExtension { @Inject private WebService webService; + @Inject + private ParticipantAgentService participantAgentService; + @Override public String name() { return NAME; @@ -76,6 +82,7 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { val objectMapper = new ObjectMapperFactory().createObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); val handlers = new SovityMessengerRegistry(); setupSovityMessengerEmitter(context, objectMapper); setupSovityMessengerReceiver(context, objectMapper, handlers); @@ -89,7 +96,7 @@ private void setupSovityMessengerEmitter(ServiceExtensionContext context, Object typeTransformerRegistry.register(new JsonObjectFromSovityMessageRequest()); - val sovityMessenger = new SovityMessenger(registry, objectMapper); + val sovityMessenger = new SovityMessenger(registry, objectMapper, monitor); context.registerService(SovityMessenger.class, sovityMessenger); } @@ -100,6 +107,7 @@ private void setupSovityMessengerReceiver(ServiceExtensionContext context, Objec typeTransformerRegistry, monitor, objectMapper, + participantAgentService, handlers); webService.registerResource(dspApiConfiguration.getContextAlias(), receiver); diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java index 933dc6a1f..317c0a392 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java @@ -9,17 +9,20 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger; -import de.sovity.edc.extension.messenger.impl.Handler; +import de.sovity.edc.extension.messenger.impl.HandlerBox; import lombok.SneakyThrows; import lombok.val; +import org.eclipse.edc.spi.iam.ClaimToken; -import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -28,7 +31,7 @@ */ public class SovityMessengerRegistry { - private final Map> handlers = new HashMap<>(); + private final Map> handlers = new HashMap<>(); /** * Register a handler to process a sovity message. @@ -41,7 +44,25 @@ public class SovityMessengerRegistry { @SneakyThrows public void register(Class incomingMessage, Function handler) { val type = getTypeViaIntrospection(incomingMessage); - register(incomingMessage, type, handler); + registerIfNotExists(incomingMessage, type, (claims, in) -> handler.apply(in)); + } + + @SneakyThrows + public void register(Class incomingMessage, BiFunction handler) { + val type = getTypeViaIntrospection(incomingMessage); + registerIfNotExists(incomingMessage, type, handler); + } + + /** + * Use this constructor only if your message can't have a default constructor. Otherwise, prefer using + * {@link #register(Class, Function)} for type safety. + */ + public void register(Class clazz, String type, Function handler) { + registerIfNotExists(clazz, type, (BiFunction) (claimToken, in) -> handler.apply(in)); + } + + public void register(Class clazz, String type, BiFunction handler) { + registerIfNotExists(clazz, type, handler); } /** @@ -53,19 +74,19 @@ public void register(Class incomingMessage, @SneakyThrows public void registerSignal(Class incomingSignal, Consumer handler) { val type = getTypeViaIntrospection(incomingSignal); - registerSignal(incomingSignal, type, handler); + registerIfNotExists(incomingSignal, type, (claims, in) -> { + handler.accept(in); + return null; + }); } - /** - * Use this constructor only if your message can't have a default constructor. Otherwise, prefer using - * {@link #register(Class, Function)} for type safety. - */ - public void register(Class clazz, String type, Function handler) { - if (handlers.containsKey(type)) { - throw new IllegalStateException("A handler is already registered for " + type); - } - - handlers.put(type, new Handler<>(clazz, handler)); + @SneakyThrows + public void registerSignal(Class incomingSignal, BiConsumer handler) { + val type = getTypeViaIntrospection(incomingSignal); + registerIfNotExists(incomingSignal, type, (claims, in) -> { + handler.accept(claims, in); + return null; + }); } /** @@ -73,11 +94,18 @@ public void register(Class clazz, String typ * {@link #registerSignal(Class, Consumer)} for type safety. */ public void registerSignal(Class clazz, String type, Consumer handler) { + registerIfNotExists(clazz, type, (claims, in) -> { + handler.accept(in); + return null; + }); + } + + public void registerSignal(Class clazz, String type, BiConsumer handler) { if (handlers.containsKey(type)) { throw new IllegalStateException("A handler is already registered for " + type); } - register(clazz, type, in -> { - handler.accept(in); + registerIfNotExists(clazz, type, (claims, in) -> { + handler.accept(claims, in); return null; }); } @@ -89,14 +117,23 @@ public void registerSignal(Class clazz, String ty * @return The function to process this message type. */ @SuppressWarnings("unchecked") - public Handler getHandler(String type) { - return (Handler) handlers.get(type); + public HandlerBox getHandler(String type) { + return (HandlerBox) handlers.get(type); } - private static String getTypeViaIntrospection(Class incomingMessage) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + @SneakyThrows + private static String getTypeViaIntrospection(Class incomingMessage) { val defaultConstructor = incomingMessage.getConstructor(); defaultConstructor.setAccessible(true); val type = defaultConstructor.newInstance().getType(); return type; } + + private void registerIfNotExists(Class clazz, String type, BiFunction handler) { + if (handlers.containsKey(type)) { + throw new IllegalStateException("A handler is already registered for " + type); + } + + handlers.put(type, new HandlerBox<>(clazz, handler)); + } } diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java index 5acc77c21..1981039aa 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.controller; @@ -17,7 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.extension.messenger.SovityMessage; import de.sovity.edc.extension.messenger.SovityMessengerRegistry; -import de.sovity.edc.extension.messenger.impl.Handler; +import de.sovity.edc.extension.messenger.impl.HandlerBox; import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; import de.sovity.edc.extension.messenger.impl.SovityMessageResponse; import de.sovity.edc.extension.messenger.impl.SovityMessengerStatus; @@ -36,6 +37,7 @@ import lombok.val; import org.eclipse.edc.protocol.dsp.api.configuration.error.DspErrorResponse; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.agent.ParticipantAgentService; import org.eclipse.edc.spi.iam.ClaimToken; import org.eclipse.edc.spi.iam.IdentityService; import org.eclipse.edc.spi.iam.TokenRepresentation; @@ -61,6 +63,7 @@ public class SovityMessageController { private final TypeTransformerRegistry typeTransformerRegistry; private final Monitor monitor; private final ObjectMapper mapper; + private final ParticipantAgentService participant; @Getter private final SovityMessengerRegistry handlers; @@ -78,6 +81,8 @@ public Response post( ).build(); } + val claims = validation.getContent(); + val handler = getHandler(request); if (handler == null) { val errorAnswer = buildErrorNoHandlerHeader(request); @@ -87,7 +92,7 @@ public Response post( } try { - val response = processMessage(request, handler); + val response = processMessage(request, handler, claims); return typeTransformerRegistry.transform(response, JsonObject.class) .map(it -> Response.ok().type(MediaType.APPLICATION_JSON).entity(it).build()) @@ -108,10 +113,10 @@ public Response post( } } - private SovityMessageResponse processMessage(SovityMessageRequest compacted, Handler handler) throws JsonProcessingException { + private SovityMessageResponse processMessage(SovityMessageRequest compacted, HandlerBox handler, ClaimToken claims) throws JsonProcessingException { val bodyStr = compacted.body(); val parsed = mapper.readValue(bodyStr, handler.clazz()); - val result = handler.handler().apply(parsed); + val result = handler.handler().apply(claims, parsed); val resultBody = mapper.writeValueAsString(result); val response = new SovityMessageResponse( @@ -165,7 +170,7 @@ private SovityMessageResponse buildErrorHandlerExceptionHeader(SovityMessageRequ return new SovityMessageResponse(headerStr, ""); } - private Handler getHandler(SovityMessageRequest request) { + private HandlerBox getHandler(SovityMessageRequest request) { final var messageType = getMessageType(request); return handlers.getHandler(messageType); } diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/HandlerBox.java similarity index 69% rename from extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java rename to extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/HandlerBox.java index 4e7ef993b..7112a803b 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/HandlerBox.java @@ -9,11 +9,14 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; -import java.util.function.Function; +import org.eclipse.edc.spi.iam.ClaimToken; + +import java.util.function.BiFunction; -public record Handler(Class clazz, Function handler) { +public record HandlerBox(Class clazz, BiFunction handler) { } diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java index baf15f102..1a3f36c78 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java index 9ae6a42e8..b8c747c24 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java index 1ba6bfb46..c6350f7b3 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java index c997b9e4b..ff53f5dde 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java index 4c794e5ba..f158e9ca8 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java index 4a057b76f..b5d330437 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java index e6c79d291..c966f187c 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java index 516c16dce..799a9ab44 100644 --- a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java index dce553b83..898c9645f 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java index e21839a13..0b4d1281c 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.controller; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java index 6d791d5bf..2d3cc5c90 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.controller; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java index c297e442a..35dd978f0 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.controller; @@ -23,6 +24,8 @@ import jakarta.ws.rs.core.Response; import lombok.val; import org.eclipse.edc.core.transform.TypeTransformerRegistryImpl; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.eclipse.edc.spi.agent.ParticipantAgentService; import org.eclipse.edc.spi.iam.ClaimToken; import org.eclipse.edc.spi.iam.IdentityService; import org.eclipse.edc.spi.monitor.ConsoleMonitor; @@ -32,9 +35,12 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.Map; import java.util.function.Function; +import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.agent.ParticipantAgentService.DEFAULT_IDENTITY_CLAIM_KEY; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; @@ -48,9 +54,15 @@ class SovityMessageControllerTest { private ObjectMapper objectMapper = omf.createObjectMapper(); private IdentityService identityService = mock(IdentityService.class); private SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + private ParticipantAgentService participantAgentService = mock(ParticipantAgentService.class); @BeforeEach public void beforeEach() { + reset( + participantAgentService, + identityService + ); + transformers = new TypeTransformerRegistryImpl(); transformers.register(new JsonObjectFromSovityMessageRequest()); transformers.register(new JsonObjectFromSovityMessageResponse()); @@ -62,7 +74,6 @@ public void beforeEach() { handlers = new SovityMessengerRegistry(); - reset(identityService); when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.success(ClaimToken.Builder.newInstance().build())); } @@ -78,6 +89,7 @@ void canAnswerRequest() throws JsonProcessingException, MalformedURLException { transformers, monitor, objectMapper, + participantAgentService, handlers ); @@ -106,7 +118,14 @@ void post_whenNonAuthorized_shouldReturnHttp401() throws MalformedURLException, val identityService = mock(IdentityService.class); when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.failure("Invalid token")); - val controller = new SovityMessageController(identityService, "http://example.com/callback", transformers, monitor, objectMapper, handlers); + val controller = new SovityMessageController( + identityService, + "http://example.com/callback", + transformers, + monitor, + objectMapper, + participantAgentService, + handlers); val message = new SovityMessageRequest( new URL("https://example.com/api"), @@ -122,4 +141,40 @@ void post_whenNonAuthorized_shouldReturnHttp401() throws MalformedURLException, } } + @Test + void canIdentifyTheEmittingEdc() throws JsonProcessingException, MalformedURLException { + // arrange + when(participantAgentService.createFor(any())) + .thenReturn(new ParticipantAgent(Map.of(DEFAULT_IDENTITY_CLAIM_KEY, "theRealEdc"), emptyMap())); + + val handlers = new SovityMessengerRegistry(); + + val controller = new SovityMessageController( + identityService, + "http://example.com/callback", + transformers, + monitor, + objectMapper, + participantAgentService, + handlers + ); + + Function handler = payload -> new Answer(String.valueOf(payload.getInteger())); + handlers.register(Payload.class, "foo", handler); + + val message = new SovityMessageRequest( + new URL("https://example.com/api"), + """ + { "type" : "foo" } + """, + objectMapper.writeValueAsString(new Payload(1))); + + // act + + try (val response = controller.post("", message)) { + // assert + assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + } + + } } diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java index 1052df2b9..a7b67d458 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo; @@ -17,10 +18,13 @@ import de.sovity.edc.extension.messenger.SovityMessengerRegistry; import de.sovity.edc.extension.messenger.demo.message.Addition; import de.sovity.edc.extension.messenger.demo.message.Answer; +import de.sovity.edc.extension.messenger.demo.message.Counterparty; import de.sovity.edc.extension.messenger.demo.message.Failing; import de.sovity.edc.extension.messenger.demo.message.Signal; import de.sovity.edc.extension.messenger.demo.message.Sqrt; +import lombok.val; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.agent.ParticipantAgentService; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; @@ -29,6 +33,9 @@ public class SovityMessengerDemo implements ServiceExtension { + @Inject + private ParticipantAgentService participantAgentService; + public static final String NAME = "sovityMessengerDemo"; @Override @@ -62,6 +69,12 @@ public void initialize(ServiceExtensionContext context) { throw new RuntimeException("Failed!"); }); + + registry.register(Counterparty.class, (claims, counterparty) -> { + val agent = participantAgentService.createFor(claims); + return new Answer(); + }); + /* * In the counterpart connector, messages can be sent with the code below. * Check out the de.sovity.edc.extension.sovitymessenger.demo.SovityMessengerDemoTest#demo() diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java index afc357de9..9a4c57992 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java @@ -9,78 +9,40 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.messenger.SovityMessenger; import de.sovity.edc.extension.messenger.SovityMessengerException; import de.sovity.edc.extension.messenger.demo.message.Addition; import de.sovity.edc.extension.messenger.demo.message.Answer; +import de.sovity.edc.extension.messenger.demo.message.Counterparty; import de.sovity.edc.extension.messenger.demo.message.Failing; import de.sovity.edc.extension.messenger.demo.message.Signal; import de.sovity.edc.extension.messenger.demo.message.Sqrt; import de.sovity.edc.extension.messenger.demo.message.UnregisteredMessage; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import lombok.SneakyThrows; import lombok.val; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.spi.iam.TokenDecorator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static java.util.concurrent.TimeUnit.SECONDS; class SovityMessengerDemoTest { - // Setup, you may skip this part - - private static final String EMITTER_PARTICIPANT_ID = "emitter"; - private static final String RECEIVER_PARTICIPANT_ID = "receiver"; - - @RegisterExtension - static EdcExtension emitterEdcContext = new EdcExtension(); - @RegisterExtension - static EdcExtension receiverEdcContext = new EdcExtension(); - - @RegisterExtension - static final TestDatabase EMITTER_DATABASE = new TestDatabaseViaTestcontainers(); - @RegisterExtension - static final TestDatabase RECEIVER_DATABASE = new TestDatabaseViaTestcontainers(); - - private ConnectorConfig providerConfig; - private ConnectorConfig consumerConfig; - - private String receiverAddress; - - // still setup, skip - - @BeforeEach - void setup() { - providerConfig = forTestDatabase(EMITTER_PARTICIPANT_ID, EMITTER_DATABASE); - emitterEdcContext.setConfiguration(providerConfig.getProperties()); - emitterEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); - - consumerConfig = forTestDatabase(RECEIVER_PARTICIPANT_ID, RECEIVER_DATABASE); - receiverEdcContext.setConfiguration(consumerConfig.getProperties()); - receiverEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); - - receiverAddress = "http://localhost:" + consumerConfig.getProtocolEndpoint().port() + consumerConfig.getProtocolEndpoint().path(); - } - - /** - * Actual usage of the Sovity Messenger. - */ @DisabledOnGithub @Test - void demo() throws ExecutionException, InterruptedException, TimeoutException { + @SneakyThrows + void demo() { /* * Get a reference to the SovityMessenger. This is equivalent to * @@ -90,21 +52,23 @@ void demo() throws ExecutionException, InterruptedException, TimeoutException { * * This messenger is already configured to accept messages in de.sovity.edc.extension.messenger.demo.SovityMessengerDemo#initialize */ - val messenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + val messenger = emitterExtension.getEdcRuntimeExtension().getContext().getService(SovityMessenger.class); System.out.println("START MARKER"); // Send messages val added = messenger.send(Answer.class, receiverAddress, new Addition(20, 30)); val rooted = messenger.send(Answer.class, receiverAddress, new Sqrt(9.0)); + val withClaims = messenger.send(Answer.class, receiverAddress, new Counterparty()); val unregistered = messenger.send(Answer.class, receiverAddress, new UnregisteredMessage()); messenger.send(receiverAddress, new Signal()); try { // Wait for the answers - added.get(2, TimeUnit.SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); - rooted.get(2, TimeUnit.SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); - unregistered.get(2, TimeUnit.SECONDS); + added.get(2, SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); + rooted.get(2, SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); + withClaims.get(2, SECONDS); + unregistered.get(2, SECONDS); } catch (ExecutionException e) { /* * When a problem happens, a SovityMessengerException is thrown and encapsulated in an ExecutionException. @@ -115,8 +79,8 @@ void demo() throws ExecutionException, InterruptedException, TimeoutException { try { val failing1 = messenger.send(Answer.class, receiverAddress, new Failing("Some content 1")); val failing2 = messenger.send(Answer.class, receiverAddress, new Failing("Some content 2")); - failing1.get(2, TimeUnit.SECONDS); - failing2.get(2, TimeUnit.SECONDS); + failing1.get(2, SECONDS); + failing2.get(2, SECONDS); } catch (ExecutionException e) { val cause = e.getCause(); if (cause instanceof SovityMessengerException messengerException) { @@ -130,4 +94,33 @@ void demo() throws ExecutionException, InterruptedException, TimeoutException { System.out.println("END MARKER"); } + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase emitterExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "emitter", + testDatabase -> { + ConnectorConfig emitterConfig = forTestDatabase("emitter", testDatabase); + return emitterConfig.getProperties(); + } + ); + + + private static ConnectorConfig receiverConfig; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase receiverExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "receiver", + testDatabase -> { + receiverConfig = forTestDatabase("receiver", testDatabase); + return receiverConfig.getProperties(); + } + ); + + private String receiverAddress; + + @BeforeEach + void setup() { + receiverAddress = receiverConfig.getProtocolEndpoint().getUri().toString(); + } } diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java index 370a5c7bf..3160cfa72 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java index 8c79d1487..ac9bb2cc6 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Counterparty.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Counterparty.java new file mode 100644 index 000000000..e2076eb28 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Counterparty.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class Counterparty implements SovityMessage { + @Override + public String getType() { + return "counterparty"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java index 17dfa78be..533555b26 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java index f82c27dc3..fc142fa84 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java index 5cf54edff..5189ccf5b 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java index b2011d507..147b1abad 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java index 5f1be9879..c7c3c5c02 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.demo.message; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java index a5f270e70..97fb90c57 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.dto; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java index 29d3fa236..5bcc85af4 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.dto; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java index e25689bb6..d3511bd4a 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.dto; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java index 9ccad0e52..377a6a30b 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.dto; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java index c8df906ee..89cb3308c 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.echo; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java index 8369c7595..1d27a3f88 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java index b2c44cd72..de996408b 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; @@ -20,6 +21,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.val; +import org.eclipse.edc.spi.iam.ClaimToken; import org.junit.jupiter.api.Test; import java.util.function.Function; @@ -54,7 +56,7 @@ void canRegisterAndRetrieveHandler() { val back = handlers.getHandler("itoa"); // assert - assertThat(back.handler().apply(new MyInt(1))).isEqualTo("1"); + assertThat(back.handler().apply(ClaimToken.Builder.newInstance().build(), new MyInt(1))).isEqualTo("1"); } @Test diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java index 6c2334d85..272d9e642 100644 --- a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.extension.messenger.impl; @@ -18,6 +19,7 @@ import de.sovity.edc.extension.messenger.dto.UnsupportedMessage; import lombok.val; import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; import org.eclipse.edc.spi.response.StatusResult; import org.junit.jupiter.api.Test; @@ -50,7 +52,7 @@ void send_whenNoHandler_shouldThrowSovityMessengerException() throws MalformedUR null))); when(registry.dispatch(any(), any())).thenReturn(future); - val messenger = new SovityMessenger(registry, new ObjectMapperFactory().createObjectMapper()); + val messenger = new SovityMessenger(registry, new ObjectMapperFactory().createObjectMapper(), new ConsoleMonitor()); val answer = messenger.send(Answer.class, "https://example.com/api/dsp", new UnsupportedMessage()); // act diff --git a/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java index 3f7777cd0..8f1ea1676 100644 --- a/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java +++ b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendController.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.testbackendcontroller; diff --git a/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java index 4d5943187..d582486d9 100644 --- a/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java +++ b/extensions/test-backend-controller/src/main/java/de/sovity/edc/extension/testbackendcontroller/TestBackendExtension.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.testbackendcontroller; diff --git a/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java b/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java index 4617344e5..7159c7a98 100644 --- a/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java +++ b/extensions/wrapper/clients/java-client-example/src/test/java/de/sovity/edc/client/examples/GreetingResourceTest.java @@ -17,14 +17,11 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.api.UseCaseApi; import de.sovity.edc.client.gen.model.KpiResult; -import de.sovity.edc.client.gen.model.TransferProcessStatesDto; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectMock; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Map; - import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; import static org.mockito.Mockito.mock; diff --git a/extensions/wrapper/wrapper-api/build.gradle.kts b/extensions/wrapper/wrapper-api/build.gradle.kts index 74003ce23..48644fa8b 100644 --- a/extensions/wrapper/wrapper-api/build.gradle.kts +++ b/extensions/wrapper/wrapper-api/build.gradle.kts @@ -15,14 +15,14 @@ dependencies { api(project(":extensions:wrapper:wrapper-common-mappers")) api(project(":extensions:wrapper:wrapper-ee-api")) - implementation(libs.jakarta.validationApi) + implementation(libs.apache.commonsLang) + implementation(libs.hibernate.validation) + implementation(libs.jakarta.el) implementation(libs.jakarta.rsApi) - implementation(libs.swagger.annotationsJakarta) - implementation(libs.swagger.jaxrs2Jakarta) implementation(libs.jakarta.servletApi) implementation(libs.jakarta.validationApi) - implementation(libs.jakarta.rsApi) - implementation(libs.apache.commonsLang) + implementation(libs.swagger.annotationsJakarta) + implementation(libs.swagger.jaxrs2Jakarta) } val openapiFileDir = "${project.buildDir}/swagger" diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index aa12367c2..9c28a1be2 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -34,7 +34,10 @@ import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; @@ -144,9 +147,13 @@ interface UiResource { @POST @Path("pages/contract-agreement-page") + @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Operation(description = "Collect filtered data for the Contract Agreement Page") - ContractAgreementPage getContractAgreementPage(@Nullable ContractAgreementPageQuery contractAgreementPageQuery); + ContractAgreementPage getContractAgreementPage( + @RequestBody(description = "If null, returns all the contract agreements.") + @Nullable ContractAgreementPageQuery contractAgreementPageQuery + ); @POST @Path("pages/contract-agreement-page/transfers") diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java index c4803a142..f836e5399 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java @@ -9,10 +9,13 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.ext.wrapper.api.ui.model; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; @@ -21,6 +24,7 @@ @Data @AllArgsConstructor @NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "Filters for querying a Contract Contract Agreement Page") public class ContractAgreementPageQuery { @Schema( diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java index 0f7cf243e..cd80f3f86 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementTerminationInfo.java @@ -9,17 +9,20 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.ext.wrapper.api.ui.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.OffsetDateTime; +@Builder @Data @AllArgsConstructor @NoArgsConstructor diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java index f249a3869..9ec952739 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminatedBy.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.ext.wrapper.api.ui.model; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java index 45a98dfcd..6f570e607 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java @@ -9,11 +9,13 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.ext.wrapper.api.ui.model; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -27,16 +29,20 @@ @RequiredArgsConstructor @Schema(description = "Data for terminating a Contract Agreement") public class ContractTerminationRequest { - - @Schema( - title = "Termination reason", - description = "A short reason why this contract was terminated", - requiredMode = Schema.RequiredMode.REQUIRED) - String reason; + public static final int MAX_REASON_SIZE = 100; + public static final int MAX_DETAIL_SIZE = 1000; @Schema( title = "Termination detail", description = "A user explanation to detail why the contract was terminated.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Size(max = MAX_DETAIL_SIZE) String detail; + + @Schema( + title = "Termination reason", + description = "A short reason why this contract was terminated", + requiredMode = Schema.RequiredMode.REQUIRED) + @Size(max = MAX_REASON_SIZE) + String reason; } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java index 652c18364..68c641516 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationStatus.java @@ -9,6 +9,7 @@ * * Contributors: * sovity GmbH - initial API and implementation + * */ package de.sovity.edc.ext.wrapper.api.ui.model; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java index 9dfa624a9..20289bfe9 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/UiCriterionOperator.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.ui.model; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java index 218b337dd..51408dfce 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -58,7 +58,7 @@ public interface UseCaseResource { @Produces(MediaType.APPLICATION_JSON) @Operation(description = "Fetch a connector's data offers") List queryCatalog( - @Valid @NotNull + @NotNull CatalogQuery catalogQuery ); diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java index 560f93ce2..8419b712a 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.model; diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java index 34ab6a955..0e31e0ff7 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.model; diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java index 5c107ab29..a783f1348 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.model; diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts index d468dadee..fe5a1bf79 100644 --- a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -27,7 +27,6 @@ dependencies { testImplementation(libs.junit.api) testImplementation(libs.junit.params) testImplementation(libs.mockito.core) - testImplementation(libs.mockito.inline) testImplementation(libs.mockito.junitJupiter) testRuntimeOnly(libs.junit.engine) } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java index 78f4fcb4d..168dc5666 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java @@ -14,9 +14,9 @@ package de.sovity.edc.ext.wrapper.api.common.mappers; +import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import de.sovity.edc.ext.wrapper.api.common.model.Expression; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java index 3ad1344e5..c65fe5243 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetEditRequestMapper.java @@ -14,10 +14,10 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.asset; +import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiDataSource; -import de.sovity.edc.ext.wrapper.api.common.model.DataSourceType; import lombok.NonNull; import lombok.RequiredArgsConstructor; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java index 936ebaf1d..e8c7cd00b 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilder.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java index 84050b94b..4ce35bc5c 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdParser.java @@ -8,15 +8,16 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.AssetJsonLdUtils; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.ShortDescriptionBuilder; -import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.DataSourceAvailability; +import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.JsonLdUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java index c0a018eba..d0ab38673 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/AssetJsonLdUtils.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java index e7f198345..d305809d9 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtils.java @@ -8,18 +8,17 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; -import lombok.RequiredArgsConstructor; import org.eclipse.edc.spi.types.domain.DataAddress; import java.util.HashMap; import java.util.Map; -@RequiredArgsConstructor public class EdcPropertyUtils { /** diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java index f9063dc11..be429af06 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/FailedMappingException.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java index a2158e37e..a182b75a0 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java index 1f59e9960..a82c2ab1f 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilder.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java index 47c75f218..7586a2081 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; @@ -19,7 +20,6 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.policy.model.AtomicConstraint; -import org.eclipse.edc.policy.model.Constraint; import org.eclipse.edc.policy.model.LiteralExpression; import java.util.List; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java index 1ac5f3ca1..405d166dc 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java index 92f0194bc..f271cd773 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java @@ -8,13 +8,13 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java index ad58bde83..3dc6e4b65 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrors.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java index 7bdc44502..92b24cfad 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java @@ -8,12 +8,12 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java index 87f593789..8a6482870 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/EdcPropertyUtilsTest.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; -import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java index 480b584f1..e1986bae0 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/ShortDescriptionBuilderTest.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java index 4f7730469..0245b6bb0 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java @@ -14,10 +14,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import org.eclipse.edc.policy.model.AndConstraint; import org.eclipse.edc.policy.model.AtomicConstraint; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java index 3c4786117..052890d4a 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapperTest.java @@ -15,8 +15,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; import com.fasterxml.jackson.databind.ObjectMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteral; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyLiteralType; import org.eclipse.edc.policy.model.Expression; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java index a217ffa88..f65d52a88 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/MappingErrorsTest.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java index 18635313f..0283d5ead 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/OperatorMapperTest.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import org.assertj.core.api.Assertions; import org.eclipse.edc.policy.model.Operator; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java index 1e28f8ebf..2151874d6 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java @@ -28,8 +28,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.Constraint; import org.eclipse.edc.policy.model.Duty; diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts index 55b88a8f2..2a080c73b 100644 --- a/extensions/wrapper/wrapper/build.gradle.kts +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -8,25 +8,37 @@ dependencies { annotationProcessor(libs.lombok) compileOnly(libs.lombok) - implementation(libs.edc.apiCore) - implementation(libs.edc.managementApiConfiguration) - implementation(libs.edc.dspHttpSpi) api(project(":extensions:wrapper:wrapper-api")) api(project(":extensions:wrapper:wrapper-common-mappers")) api(project(":utils:catalog-parser")) api(project(":utils:json-and-jsonld-utils")) + api(libs.edc.contractDefinitionApi) api(libs.edc.controlPlaneSpi) api(libs.edc.coreSpi) api(libs.edc.policyDefinitionApi) api(libs.edc.transferProcessApi) + + implementation(project(":extensions:contract-termination")) + implementation(project(":extensions:database-direct-access")) + implementation(project(":extensions:sovity-messenger")) + implementation(project(":utils:jooq-database-access")) + implementation(libs.apache.commonsLang) + implementation(libs.edc.apiCore) + implementation(libs.edc.managementApiConfiguration) + implementation(libs.edc.dspHttpSpi) + implementation(libs.jooq.jooq) + implementation(libs.hibernate.validation) + implementation(libs.hikari) + implementation(libs.jakarta.el) testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) testImplementation(project(":extensions:wrapper:clients:java-client")) testImplementation(project(":extensions:policy-always-true")) + testImplementation(project(":extensions:postgres-flyway")) testImplementation(project(":utils:test-utils")) testImplementation(libs.edc.controlPlaneCore) testImplementation(libs.edc.dsp) @@ -44,10 +56,13 @@ dependencies { // Updated jetty versions for e.g. CVE-2023-26048 testImplementation(libs.bundles.jetty.cve2023) - testImplementation(libs.edc.jsonLd) + testImplementation(libs.edc.controlPlaneSql) + testImplementation(libs.edc.contractNegotiationStoreSql) testImplementation(libs.edc.dspHttpSpi) testImplementation(libs.edc.dspApiConfiguration) testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.edc.jsonLd) + testImplementation(libs.edc.transferProcessStoreSql) testImplementation(libs.jsonUnit.assertj) testImplementation(libs.restAssured.restAssured) diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java index 82c993a55..8f2b29210 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java @@ -17,6 +17,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import de.sovity.edc.extension.messenger.SovityMessenger; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; @@ -42,7 +45,9 @@ public class WrapperExtension implements ServiceExtension { + public static final String EXTENSION_NAME = "WrapperExtension"; + @Inject private AssetIndex assetIndex; @Inject @@ -52,18 +57,24 @@ public class WrapperExtension implements ServiceExtension { @Inject private ContractAgreementService contractAgreementService; @Inject + private ContractAgreementTerminationService contractAgreementTerminationService; + @Inject private ContractDefinitionStore contractDefinitionStore; @Inject private ContractNegotiationService contractNegotiationService; @Inject private ContractNegotiationStore contractNegotiationStore; @Inject + private DslContextFactory dslContextFactory; + @Inject private ManagementApiConfiguration dataManagementApiConfiguration; @Inject private PolicyDefinitionStore policyDefinitionStore; @Inject private PolicyEngine policyEngine; @Inject + private SovityMessenger sovityMessenger; + @Inject private TransferProcessService transferProcessService; @Inject private TransferProcessStore transferProcessStore; @@ -91,30 +102,33 @@ public void initialize(ServiceExtensionContext context) { fixObjectMapperDateSerialization(objectMapper); var wrapperExtensionContext = WrapperExtensionContextBuilder.buildContext( - assetIndex, - assetService, - catalogService, - context.getConfig(), - contractAgreementService, - contractDefinitionService, - contractDefinitionStore, - contractNegotiationService, - contractNegotiationStore, - jsonLd, - context.getMonitor(), - objectMapper, - policyDefinitionService, - policyDefinitionStore, - policyEngine, - transferProcessService, - transferProcessStore, - typeTransformerRegistry + assetIndex, + assetService, + catalogService, + context.getConfig(), + contractAgreementService, + contractAgreementTerminationService, + contractDefinitionService, + contractDefinitionStore, + contractNegotiationService, + contractNegotiationStore, + dslContextFactory, + jsonLd, + context.getMonitor(), + objectMapper, + policyDefinitionService, + policyDefinitionStore, + policyEngine, + sovityMessenger, + transferProcessService, + transferProcessStore, + typeTransformerRegistry ); wrapperExtensionContext.selfDescriptionService().validateSelfDescriptionConfig(); wrapperExtensionContext.jaxRsResources().forEach(resource -> - webService.registerResource(dataManagementApiConfiguration.getContextAlias(), resource)); + webService.registerResource(dataManagementApiConfiguration.getContextAlias(), resource)); } private void fixObjectMapperDateSerialization(ObjectMapper objectMapper) { diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 36074d5ed..5a1d41324 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -38,6 +38,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.CatalogApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.UiDataOfferBuilder; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTerminationApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTransferApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementDataFetcher; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementPageCardBuilder; @@ -71,6 +72,11 @@ import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import de.sovity.edc.extension.contacttermination.query.ContractAgreementTerminationDetailsQuery; +import de.sovity.edc.extension.contacttermination.query.TerminateContractQuery; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import de.sovity.edc.extension.messenger.SovityMessenger; import de.sovity.edc.utils.catalog.DspCatalogService; import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; import lombok.NoArgsConstructor; @@ -109,24 +115,27 @@ public class WrapperExtensionContextBuilder { public static WrapperExtensionContext buildContext( - AssetIndex assetIndex, - AssetService assetService, - CatalogService catalogService, - Config config, - ContractAgreementService contractAgreementService, - ContractDefinitionService contractDefinitionService, - ContractDefinitionStore contractDefinitionStore, - ContractNegotiationService contractNegotiationService, - ContractNegotiationStore contractNegotiationStore, - JsonLd jsonLd, - Monitor monitor, - ObjectMapper objectMapper, - PolicyDefinitionService policyDefinitionService, - PolicyDefinitionStore policyDefinitionStore, - PolicyEngine policyEngine, - TransferProcessService transferProcessService, - TransferProcessStore transferProcessStore, - TypeTransformerRegistry typeTransformerRegistry + AssetIndex assetIndex, + AssetService assetService, + CatalogService catalogService, + Config config, + ContractAgreementService contractAgreementService, + ContractAgreementTerminationService contractAgreementTerminationService, + ContractDefinitionService contractDefinitionService, + ContractDefinitionStore contractDefinitionStore, + ContractNegotiationService contractNegotiationService, + ContractNegotiationStore contractNegotiationStore, + DslContextFactory dslContextFactory, + JsonLd jsonLd, + Monitor monitor, + ObjectMapper objectMapper, + PolicyDefinitionService policyDefinitionService, + PolicyDefinitionStore policyDefinitionStore, + PolicyEngine policyEngine, + SovityMessenger sovityMessenger, + TransferProcessService transferProcessService, + TransferProcessStore transferProcessStore, + TypeTransformerRegistry typeTransformerRegistry ) { // UI API var operatorMapper = new OperatorMapper(); @@ -138,188 +147,193 @@ public static WrapperExtensionContext buildContext( var policyValidator = new PolicyValidator(); var constraintExtractor = new ConstraintExtractor(policyValidator, atomicConstraintMapper); var policyMapper = new PolicyMapper( - constraintExtractor, - atomicConstraintMapper, - typeTransformerRegistry); + constraintExtractor, + atomicConstraintMapper, + typeTransformerRegistry); var edcPropertyUtils = new EdcPropertyUtils(); var selfDescriptionService = new SelfDescriptionService(config, monitor); var ownConnectorEndpointService = new OwnConnectorEndpointServiceImpl(selfDescriptionService); var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd, ownConnectorEndpointService); var transferProcessStateService = new TransferProcessStateService(); var contractNegotiationUtils = new ContractNegotiationUtils( - contractNegotiationService, - selfDescriptionService + contractNegotiationService, + selfDescriptionService ); var contractAgreementPageCardBuilder = new ContractAgreementPageCardBuilder( - policyMapper, - transferProcessStateService, - assetMapper, - contractNegotiationUtils + policyMapper, + transferProcessStateService, + assetMapper, + contractNegotiationUtils ); var contractAgreementDataFetcher = new ContractAgreementDataFetcher( - contractAgreementService, - contractNegotiationStore, - transferProcessService, - assetIndex + contractAgreementService, + contractNegotiationStore, + transferProcessService, + assetIndex ); var contractAgreementApiService = new ContractAgreementPageApiService( - contractAgreementDataFetcher, - contractAgreementPageCardBuilder + contractAgreementDataFetcher, + contractAgreementPageCardBuilder ); var contactDefinitionBuilder = new ContractDefinitionBuilder(criterionMapper); var contractDefinitionApiService = new ContractDefinitionApiService( - contractDefinitionService, - criterionMapper, - contactDefinitionBuilder); + contractDefinitionService, + criterionMapper, + contactDefinitionBuilder); var transferHistoryPageApiService = new TransferHistoryPageApiService( - assetService, - contractAgreementService, - contractNegotiationStore, - transferProcessService, - transferProcessStateService + assetService, + contractAgreementService, + contractNegotiationStore, + transferProcessService, + transferProcessStateService ); var transferHistoryPageAssetFetcherService = new TransferHistoryPageAssetFetcherService( - assetService, - transferProcessService, - assetMapper, - contractNegotiationStore, - contractNegotiationUtils + assetService, + transferProcessService, + assetMapper, + contractNegotiationStore, + contractNegotiationUtils ); var contractAgreementUtils = new ContractAgreementUtils(contractAgreementService); var parameterizationCompatibilityUtils = new ParameterizationCompatibilityUtils(); var assetIdValidator = new AssetIdValidator(); var assetApiService = new AssetApiService( - assetService, - assetMapper, - assetIdValidator, - selfDescriptionService + assetService, + assetMapper, + assetIdValidator, + selfDescriptionService ); var transferRequestBuilder = new TransferRequestBuilder( - contractAgreementUtils, - contractNegotiationUtils, - edcPropertyUtils, - typeTransformerRegistry, - parameterizationCompatibilityUtils + contractAgreementUtils, + contractNegotiationUtils, + edcPropertyUtils, + typeTransformerRegistry, + parameterizationCompatibilityUtils ); var contractAgreementTransferApiService = new ContractAgreementTransferApiService( - transferRequestBuilder, - transferProcessService + transferRequestBuilder, + transferProcessService ); + var agreementDetailsQuery = new ContractAgreementTerminationDetailsQuery(); + var terminateContractQuery = new TerminateContractQuery(); + var contractAgreementTerminationApiService = new ContractAgreementTerminationApiService(contractAgreementTerminationService); var policyDefinitionApiService = new PolicyDefinitionApiService( - policyDefinitionService, - policyMapper + policyDefinitionService, + policyMapper ); var dataOfferBuilder = new DspDataOfferBuilder(jsonLd); var uiDataOfferBuilder = new UiDataOfferBuilder(assetMapper, policyMapper); var dspCatalogService = new DspCatalogService(catalogService, dataOfferBuilder); var catalogApiService = new CatalogApiService( - uiDataOfferBuilder, - dspCatalogService + uiDataOfferBuilder, + dspCatalogService ); var contractOfferMapper = new ContractOfferMapper(policyMapper); var contractNegotiationBuilder = new ContractNegotiationBuilder(contractOfferMapper); var contractNegotiationStateService = new ContractNegotiationStateService(); var contractNegotiationApiService = new ContractNegotiationApiService( - contractNegotiationService, - contractNegotiationBuilder, - contractNegotiationStateService + contractNegotiationService, + contractNegotiationBuilder, + contractNegotiationStateService ); var miwConfigBuilder = new MiwConfigService(config); var dapsConfigBuilder = new DapsConfigService(config); var dashboardDataFetcher = new DashboardDataFetcher( - contractNegotiationStore, - transferProcessService, - assetIndex, - policyDefinitionService, - contractDefinitionService + contractNegotiationStore, + transferProcessService, + assetIndex, + policyDefinitionService, + contractDefinitionService ); var dashboardApiService = new DashboardPageApiService( - dashboardDataFetcher, - transferProcessStateService, - dapsConfigBuilder, - miwConfigBuilder, - selfDescriptionService + dashboardDataFetcher, + transferProcessStateService, + dapsConfigBuilder, + miwConfigBuilder, + selfDescriptionService ); var uiResource = new UiResourceImpl( - contractAgreementApiService, - contractAgreementTransferApiService, - transferHistoryPageApiService, - transferHistoryPageAssetFetcherService, - assetApiService, - policyDefinitionApiService, - catalogApiService, - contractDefinitionApiService, - contractNegotiationApiService, - dashboardApiService + contractAgreementApiService, + contractAgreementTransferApiService, + contractAgreementTerminationApiService, + transferHistoryPageApiService, + transferHistoryPageAssetFetcherService, + assetApiService, + policyDefinitionApiService, + catalogApiService, + contractDefinitionApiService, + contractNegotiationApiService, + dashboardApiService, + dslContextFactory ); // Use Case API var filterExpressionOperatorMapper = new FilterExpressionOperatorMapper(); var filterExpressionLiteralMapper = new FilterExpressionLiteralMapper(); var filterExpressionMapper = new FilterExpressionMapper( - filterExpressionOperatorMapper, - filterExpressionLiteralMapper + filterExpressionOperatorMapper, + filterExpressionLiteralMapper ); var kpiApiService = new KpiApiService( - assetIndex, - policyDefinitionStore, - contractDefinitionStore, - transferProcessStore, - contractAgreementService, - transferProcessStateService + assetIndex, + policyDefinitionStore, + contractDefinitionStore, + transferProcessStore, + contractAgreementService, + transferProcessStateService ); var supportedPolicyApiService = new SupportedPolicyApiService(policyEngine); var useCaseCatalogApiService = new UseCaseCatalogApiService( - uiDataOfferBuilder, - dspCatalogService, - filterExpressionMapper + uiDataOfferBuilder, + dspCatalogService, + filterExpressionMapper ); var useCaseResource = new UseCaseResourceImpl( - kpiApiService, - supportedPolicyApiService, - useCaseCatalogApiService, - policyDefinitionApiService + kpiApiService, + supportedPolicyApiService, + useCaseCatalogApiService, + policyDefinitionApiService ); // Collect all JAX-RS resources return new WrapperExtensionContext(List.of( - uiResource, - useCaseResource + uiResource, + useCaseResource ), selfDescriptionService); } @NotNull private static AssetMapper newAssetMapper( - TypeTransformerRegistry typeTransformerRegistry, - JsonLd jsonLd, - OwnConnectorEndpointService ownConnectorEndpointService + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd, + OwnConnectorEndpointService ownConnectorEndpointService ) { var edcPropertyUtils = new EdcPropertyUtils(); var assetJsonLdUtils = new AssetJsonLdUtils(); var assetEditRequestMapper = new AssetEditRequestMapper(); var shortDescriptionBuilder = new ShortDescriptionBuilder(); var assetJsonLdParser = new AssetJsonLdParser( - assetJsonLdUtils, - shortDescriptionBuilder, - ownConnectorEndpointService + assetJsonLdUtils, + shortDescriptionBuilder, + ownConnectorEndpointService ); var httpHeaderMapper = new HttpHeaderMapper(); var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); var dataSourceMapper = new DataSourceMapper( - edcPropertyUtils, - httpDataSourceMapper + edcPropertyUtils, + httpDataSourceMapper ); var assetJsonLdBuilder = new AssetJsonLdBuilder( - dataSourceMapper, - assetJsonLdParser, - assetEditRequestMapper + dataSourceMapper, + assetJsonLdParser, + assetEditRequestMapper ); return new AssetMapper( - typeTransformerRegistry, - assetJsonLdBuilder, - assetJsonLdParser, - jsonLd + typeTransformerRegistry, + assetJsonLdBuilder, + assetJsonLdParser, + jsonLd ); } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 872c4a651..158d8f559 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -21,10 +21,10 @@ import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPageQuery; -import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; @@ -36,6 +36,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.catalog.CatalogApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTerminationApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.ContractAgreementTransferApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractNegotiationApiService; @@ -43,17 +44,25 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; import lombok.RequiredArgsConstructor; +import lombok.val; import org.jetbrains.annotations.Nullable; import java.util.List; +import static de.sovity.edc.ext.wrapper.utils.ValidatorUtils.validate; + @SuppressWarnings("java:S6539") // This class is so large so the generated API Clients can have one UiApi @RequiredArgsConstructor public class UiResourceImpl implements UiResource { private final ContractAgreementPageApiService contractAgreementApiService; private final ContractAgreementTransferApiService contractAgreementTransferApiService; + private final ContractAgreementTerminationApiService contractAgreementTerminationApiService; private final TransferHistoryPageApiService transferHistoryPageApiService; private final TransferHistoryPageAssetFetcherService transferHistoryPageAssetFetcherService; private final AssetApiService assetApiService; @@ -62,6 +71,7 @@ public class UiResourceImpl implements UiResource { private final ContractDefinitionApiService contractDefinitionApiService; private final ContractNegotiationApiService contractNegotiationApiService; private final DashboardPageApiService dashboardPageApiService; + private final DslContextFactory dslContextFactory; @Override public DashboardPage getDashboardPage() { @@ -135,10 +145,10 @@ public UiContractNegotiation getContractNegotiation(String contractNegotiationId @Override public ContractAgreementPage getContractAgreementPage(@Nullable ContractAgreementPageQuery contractAgreementPageQuery) { - return contractAgreementApiService.contractAgreementPage(); + return dslContextFactory.transactionResult(dsl -> + contractAgreementApiService.contractAgreementPage(dsl, contractAgreementPageQuery)); } - @Override public IdResponseDto initiateTransfer(InitiateTransferRequest request) { return contractAgreementTransferApiService.initiateTransfer(request); @@ -150,8 +160,13 @@ public IdResponseDto initiateCustomTransfer(InitiateCustomTransferRequest reques } @Override - public IdResponseDto terminateContractAgreement(String contractAgreementId, ContractTerminationRequest contractTerminationRequest) { - return null; + public IdResponseDto terminateContractAgreement( + String contractAgreementId, + ContractTerminationRequest contractTerminationRequest + ) { + validate(contractTerminationRequest); + return dslContextFactory.transactionResult(dsl -> + contractAgreementTerminationApiService.terminate(dsl, contractAgreementId, contractTerminationRequest)); } @Override diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java index 8487f8eab..60a597b8b 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java @@ -16,10 +16,13 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPageQuery; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementDataFetcher; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementPageCardBuilder; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jooq.DSLContext; import java.util.Comparator; @@ -29,15 +32,25 @@ public class ContractAgreementPageApiService { private final ContractAgreementPageCardBuilder contractAgreementPageCardBuilder; @NotNull - public ContractAgreementPage contractAgreementPage() { - var agreements = contractAgreementDataFetcher.getContractAgreements(); + public ContractAgreementPage contractAgreementPage(DSLContext dsl, @Nullable ContractAgreementPageQuery contractAgreementPageQuery) { + var agreements = contractAgreementDataFetcher.getContractAgreements(dsl); var cards = agreements.stream() - .map(agreement -> contractAgreementPageCardBuilder.buildContractAgreementCard( - agreement.agreement(), agreement.negotiation(), agreement.asset(), agreement.transfers())) - .sorted(Comparator.comparing(ContractAgreementCard::getContractSigningDate).reversed()) - .toList(); + .map(agreement -> contractAgreementPageCardBuilder.buildContractAgreementCard( + agreement.agreement(), + agreement.negotiation(), + agreement.asset(), + agreement.transfers(), + agreement.termination())) + .sorted(Comparator.comparing(ContractAgreementCard::getContractSigningDate).reversed()); - return new ContractAgreementPage(cards); + if (contractAgreementPageQuery == null || contractAgreementPageQuery.getTerminationStatus() == null) { + return new ContractAgreementPage(cards.toList()); + } else { + var filtered = cards.filter(card -> + card.getTerminationStatus().equals(contractAgreementPageQuery.getTerminationStatus())) + .toList(); + return new ContractAgreementPage(filtered); + } } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTerminationApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTerminationApiService.java new file mode 100644 index 000000000..0c4ac4a7f --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementTerminationApiService.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements; + +import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import de.sovity.edc.extension.contacttermination.ContractTerminationParam; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.spi.EdcException; +import org.jooq.DSLContext; + +@RequiredArgsConstructor +public class ContractAgreementTerminationApiService { + + private final ContractAgreementTerminationService contractAgreementTerminationService; + + public IdResponseDto terminate( + DSLContext dsl, + String contractAgreementId, + ContractTerminationRequest contractTerminationRequest) { + + try { + val terminatedAt = contractAgreementTerminationService.terminateAgreementOrThrow( + dsl, + new ContractTerminationParam( + contractAgreementId, + contractTerminationRequest.getDetail(), + contractTerminationRequest.getReason())); + + return IdResponseDto.builder() + .id(contractAgreementId) + .lastUpdatedDate(terminatedAt) + .build(); + + } catch (RuntimeException e) { + throw new EdcException("Failed to terminate the agreement %s".formatted(contractAgreementId) + " : " + e.getMessage(), e); + } + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java index 77f26eefa..6e3411762 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java @@ -14,12 +14,14 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; +import de.sovity.edc.ext.db.jooq.tables.records.SovityContractTerminationRecord; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; import org.eclipse.edc.spi.types.domain.asset.Asset; import java.util.List; +import java.util.Map; /** @@ -34,7 +36,8 @@ public record ContractAgreementData( ContractAgreement agreement, ContractNegotiation negotiation, Asset asset, - List transfers + List transfers, + SovityContractTerminationRecord termination ) { } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java index 4e9b19e15..a0f45c062 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; +import de.sovity.edc.ext.db.jooq.tables.records.SovityContractTerminationRecord; import de.sovity.edc.ext.wrapper.api.ServiceException; import de.sovity.edc.ext.wrapper.utils.MapUtils; import lombok.RequiredArgsConstructor; @@ -27,11 +28,15 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; import java.util.List; import java.util.Map; +import static de.sovity.edc.ext.db.jooq.Tables.SOVITY_CONTRACT_TERMINATION; +import static java.util.function.Function.identity; import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; @RequiredArgsConstructor public class ContractAgreementDataFetcher { @@ -46,26 +51,46 @@ public class ContractAgreementDataFetcher { * @return {@link ContractAgreementData}s */ @NotNull - public List getContractAgreements() { + public List getContractAgreements(DSLContext dsl) { var agreements = getAllContractAgreements(); var assets = MapUtils.associateBy(getAllAssets(), Asset::getId); var negotiations = getAllContractNegotiations().stream() - .filter(it -> it.getContractAgreement() != null) - .collect(groupingBy(it -> it.getContractAgreement().getId())); + .filter(it -> it.getContractAgreement() != null) + .collect(groupingBy(it -> it.getContractAgreement().getId())); var transfers = getAllTransferProcesses().stream() - .collect(groupingBy(it -> it.getDataRequest().getContractId())); + .collect(groupingBy(it -> it.getDataRequest().getContractId())); + + var terminations = fetchTerminations(dsl, agreements); // A ContractAgreement has multiple ContractNegotiations when doing a loopback consumption return agreements.stream() - .flatMap(agreement -> negotiations.getOrDefault(agreement.getId(), List.of()).stream() - .map(negotiation -> { - var asset = getAsset(agreement, negotiation, assets); - var contractTransfers = transfers.getOrDefault(agreement.getId(), List.of()); - return new ContractAgreementData(agreement, negotiation, asset, contractTransfers); - })) - .toList(); + .flatMap(agreement -> negotiations.getOrDefault(agreement.getId(), List.of()) + .stream() + .map(negotiation -> { + var asset = getAsset(agreement, negotiation, assets); + var contractTransfers = transfers.getOrDefault(agreement.getId(), List.of()); + return new ContractAgreementData(agreement, negotiation, asset, contractTransfers, terminations.get(agreement.getId())); + })) + .toList(); + } + + private @NotNull Map fetchTerminations(DSLContext dsl, List agreements) { + + var agreementIds = agreements.stream().map(ContractAgreement::getId).toList(); + + var t = SOVITY_CONTRACT_TERMINATION; + + var terminations = dsl.select() + .from(t) + .where(t.CONTRACT_AGREEMENT_ID.in(agreementIds)) + .fetch() + .into(t) + .stream() + .collect(toMap(SovityContractTerminationRecord::getContractAgreementId, identity())); + + return terminations; } private Asset getAsset(ContractAgreement agreement, ContractNegotiation negotiation, Map assets) { diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java index f7a7c2644..7e35f1a3d 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java @@ -14,14 +14,16 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services; +import de.sovity.edc.ext.db.jooq.tables.records.SovityContractTerminationRecord; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementDirection; -import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTransferProcess; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTerminationInfo; -import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationStatus; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTransferProcess; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminatedBy; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; +import jakarta.validation.constraints.Null; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; @@ -32,7 +34,10 @@ import java.util.Comparator; import java.util.List; +import java.util.Map; +import static de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationStatus.ONGOING; +import static de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationStatus.TERMINATED; import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcMillisToOffsetDateTime; import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcSecondsToOffsetDateTime; @@ -45,11 +50,13 @@ public class ContractAgreementPageCardBuilder { @NotNull public ContractAgreementCard buildContractAgreementCard( - @NonNull ContractAgreement agreement, - @NonNull ContractNegotiation negotiation, - @NonNull Asset asset, - @NonNull List transferProcesses + @NonNull ContractAgreement agreement, + @NonNull ContractNegotiation negotiation, + @NonNull Asset asset, + @NonNull List transferProcesses, + SovityContractTerminationRecord termination ) { + var assetParticipantId = contractNegotiationUtils.getProviderParticipantId(negotiation); var assetConnectorEndpoint = contractNegotiationUtils.getProviderConnectorEndpoint(negotiation); @@ -63,32 +70,51 @@ public ContractAgreementCard buildContractAgreementCard( card.setAsset(assetMapper.buildUiAsset(asset, assetConnectorEndpoint, assetParticipantId)); card.setContractPolicy(policyMapper.buildUiPolicy(agreement.getPolicy())); card.setTransferProcesses(buildTransferProcesses(transferProcesses)); - card.setTerminationStatus(ContractTerminationStatus.ONGOING); - card.setTerminationInformation(null); + + addTermination(termination, card); + return card; } + private static void addTermination(SovityContractTerminationRecord termination, ContractAgreementCard card) { + if (termination != null) { + card.setTerminationStatus(TERMINATED); + card.setTerminationInformation(ContractAgreementTerminationInfo.builder() + .detail(termination.getDetail()) + .reason(termination.getReason()) + .terminatedAt(termination.getTerminatedAt()) + .terminatedBy(switch (termination.getTerminatedBy()) { + case SELF -> ContractTerminatedBy.SELF; + case COUNTERPARTY -> ContractTerminatedBy.COUNTERPARTY; + }) + .build()); + } else { + card.setTerminationStatus(ONGOING); + card.setTerminationInformation(null); + } + } + @NotNull private List buildTransferProcesses( - @NonNull List transferProcessEntities + @NonNull List transferProcessEntities ) { return transferProcessEntities.stream() - .map(this::buildContractAgreementTransfer) - .sorted(Comparator.comparing(ContractAgreementTransferProcess::getLastUpdatedDate) - .reversed()) - .toList(); + .map(this::buildContractAgreementTransfer) + .sorted(Comparator.comparing(ContractAgreementTransferProcess::getLastUpdatedDate) + .reversed()) + .toList(); } @NotNull - private ContractAgreementTransferProcess buildContractAgreementTransfer( - TransferProcess transferProcessEntity) { + private ContractAgreementTransferProcess buildContractAgreementTransfer(TransferProcess transferProcessEntity) { + var transferProcess = new ContractAgreementTransferProcess(); + transferProcess.setTransferProcessId(transferProcessEntity.getId()); - transferProcess.setLastUpdatedDate( - utcMillisToOffsetDateTime(transferProcessEntity.getUpdatedAt())); - transferProcess.setState(transferProcessStateService.buildTransferProcessState( - transferProcessEntity.getState())); + transferProcess.setLastUpdatedDate(utcMillisToOffsetDateTime(transferProcessEntity.getUpdatedAt())); + transferProcess.setState(transferProcessStateService.buildTransferProcessState(transferProcessEntity.getState())); transferProcess.setErrorMessage(transferProcessEntity.getErrorDetail()); + return transferProcess; } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java index e63b10170..0ce0b737e 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java @@ -17,10 +17,10 @@ import de.sovity.edc.ext.wrapper.api.ServiceException; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java index a00d3552c..f3a295e4d 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageAssetFetcherService.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java index c743faa64..076c0db12 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java @@ -14,12 +14,12 @@ package de.sovity.edc.ext.wrapper.api.usecase; -import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; +import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/ValidatorUtils.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/ValidatorUtils.java new file mode 100644 index 000000000..c7b292f21 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/utils/ValidatorUtils.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.utils; + +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; +import jakarta.ws.rs.BadRequestException; +import lombok.val; + +public class ValidatorUtils { + private static final ValidatorFactory FACTORY = Validation.buildDefaultValidatorFactory(); + + public static void validate(Object object) { + val validator = FACTORY.getValidator(); + val constraintViolations = validator.validate(object); + if (!constraintViolations.isEmpty()) { + throw new BadRequestException(new ConstraintViolationException("Failed to validate", constraintViolations)); + } + } +} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java deleted file mode 100644 index 12054c816..000000000 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/TestUtils.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.wrapper; - -import de.sovity.edc.client.EdcClient; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.spi.protocol.ProtocolWebhook; - -import java.util.HashMap; -import java.util.Map; - -public class TestUtils { - private static final int MANAGEMENT_PORT = 34002; - private static final int PROTOCOL_PORT = 34003; - private static final int WEB_PORT = 34001; - private static final String MANAGEMENT_PATH = "/api/management"; - private static final String PROTOCOL_PATH = "/api/dsp"; - public static final String MANAGEMENT_API_KEY = "123456"; - public static final String MANAGEMENT_ENDPOINT = "http://localhost:" + MANAGEMENT_PORT + MANAGEMENT_PATH; - - - public static final String PROTOCOL_HOST = "http://localhost:" + PROTOCOL_PORT; - public static final String PROTOCOL_ENDPOINT = PROTOCOL_HOST + PROTOCOL_PATH; - - public static Map createConfiguration( - Map additionalConfigProperties - ) { - Map config = new HashMap<>(); - config.put("web.http.port", String.valueOf(WEB_PORT)); - config.put("web.http.path", "/api"); - config.put("web.http.management.port", String.valueOf(MANAGEMENT_PORT)); - config.put("web.http.management.path", MANAGEMENT_PATH); - config.put("web.http.protocol.port", String.valueOf(PROTOCOL_PORT)); - config.put("web.http.protocol.path", PROTOCOL_PATH); - config.put("edc.api.auth.key", MANAGEMENT_API_KEY); - config.put("edc.dsp.callback.address", PROTOCOL_ENDPOINT); - config.put("edc.oauth.provider.audience", "idsc:IDS_CONNECTORS_ALL"); - - config.put("edc.participant.id", "my-edc-participant-id"); - config.put("my.edc.participant.id", "my-edc-participant-id"); - config.put("my.edc.title", "My Connector"); - config.put("my.edc.description", "My Connector Description"); - config.put("my.edc.curator.url", "https://connector.my-org"); - config.put("my.edc.curator.name", "My Org"); - config.put("my.edc.maintainer.url", "https://maintainer-org"); - config.put("my.edc.maintainer.name", "Maintainer Org"); - - config.put("edc.oauth.token.url", "https://token-url.daps"); - config.put("edc.oauth.provider.jwks.url", "https://jwks-url.daps"); - config.put("tx.ssi.miw.authority.id", "my-authority-id"); - config.put("tx.ssi.miw.url", "https://miw"); - config.put("tx.ssi.oauth.token.url", "https://token.miw"); - config.putAll(additionalConfigProperties); - return config; - } - - public static void setupExtension(EdcExtension extension) { - System.out.println("Hello World from TestUtils#setupExtension!"); - setupExtension(extension, Map.of()); - } - - public static void setupExtension(EdcExtension extension, Map configProperties) { - extension.registerServiceMock(ProtocolWebhook.class, () -> PROTOCOL_ENDPOINT); - extension.setConfiguration(createConfiguration(configProperties)); - } - - public static EdcClient edcClient() { - return EdcClient.builder() - .managementApiUrl(TestUtils.MANAGEMENT_ENDPOINT) - .managementApiKey(TestUtils.MANAGEMENT_API_KEY) - .build(); - } -} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java index 08922d047..f11cee68b 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java @@ -21,20 +21,20 @@ import de.sovity.edc.client.gen.model.UiAssetEditRequest; import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; -import de.sovity.edc.ext.wrapper.TestUtils; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.EdcPropertyUtils; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.utils.jsonld.vocab.Prop; import lombok.SneakyThrows; +import lombok.val; import org.eclipse.edc.connector.spi.asset.AssetService; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.text.SimpleDateFormat; import java.time.LocalDate; @@ -42,32 +42,47 @@ import java.util.List; import java.util.Map; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) public class AssetApiServiceTest { + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "edc", + testDatabase -> { + val config = forTestDatabase("MyEDC", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); + public static final String DATA_SINK = "http://my-data-sink/api/stuff"; - EdcClient client; EdcPropertyUtils edcPropertyUtils; @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); + void setup() { edcPropertyUtils = new EdcPropertyUtils(); - client = TestUtils.edcClient(); } @Test - void assetPage(AssetService assetStore) { + void assetPage() { + val assetService = providerExtension.getEdcRuntimeExtension().getContext().getService(AssetService.class); + // arrange var properties = Map.of( - Asset.PROPERTY_ID, "asset-1", - Prop.Dcat.LANDING_PAGE, "https://data-source.my-org/docs" + Asset.PROPERTY_ID, "asset-11", + Prop.Dcat.LANDING_PAGE, "https://data-source.my-org/docs" ); - createAsset(assetStore, "2023-06-01", properties); + createAsset(assetService, "2023-06-01", properties); // act var result = client.uiApi().getAssetPage(); @@ -81,23 +96,27 @@ void assetPage(AssetService assetStore) { } @Test - void assetPageSorting(AssetService assetService) { + void assetPageSorting() { + val assetService = providerExtension.getEdcRuntimeExtension().getContext().getService(AssetService.class); + // arrange - createAsset(assetService, "2023-06-01", Map.of(Asset.PROPERTY_ID, "asset-1")); - createAsset(assetService, "2023-06-03", Map.of(Asset.PROPERTY_ID, "asset-3")); - createAsset(assetService, "2023-06-02", Map.of(Asset.PROPERTY_ID, "asset-2")); + createAsset(assetService, "2023-06-01", Map.of(Asset.PROPERTY_ID, "asset-21")); + createAsset(assetService, "2023-06-03", Map.of(Asset.PROPERTY_ID, "asset-23")); + createAsset(assetService, "2023-06-02", Map.of(Asset.PROPERTY_ID, "asset-22")); // act var result = client.uiApi().getAssetPage(); // assert assertThat(result.getAssets()) - .extracting(UiAsset::getAssetId) - .containsExactly("asset-3", "asset-2", "asset-1"); + .extracting(UiAsset::getAssetId) + .containsExactly("asset-23", "asset-22", "asset-21"); } @Test - void testAssetCreation(AssetService assetService) { + void testAssetCreation() { + val assetService = providerExtension.getEdcRuntimeExtension().getContext().getService(AssetService.class); + // arrange var dataSource = UiDataSource.builder() .type(DataSourceType.HTTP_DATA) @@ -111,65 +130,65 @@ void testAssetCreation(AssetService assetService) { .customProperties(Map.of("oauth2:tokenUrl", "https://token-url")) .build(); var uiAssetRequest = UiAssetCreateRequest.builder() - .id("asset-1") - .title("AssetTitle") - .description("AssetDescription") - .licenseUrl("https://license-url") - .version("1.0.0") - .language("en") - .mediaType("application/json") - .dataCategory("dataCategory") - .dataSubcategory("dataSubcategory") - .dataModel("dataModel") - .geoReferenceMethod("geoReferenceMethod") - .transportMode("transportMode") - .sovereignLegalName("my sovereign") - .geoLocation("40.0, 40.0") - .nutsLocations(Arrays.asList("DE", "DE929")) - .dataSampleUrls(Arrays.asList("https://sample-a", "https://sample-b")) - .referenceFileUrls(Arrays.asList("https://reference-a", "https://reference-b")) - .referenceFilesDescription("RF Description") - .conditionsForUse("Conditions for use") - .dataUpdateFrequency("every month") - .temporalCoverageFrom(LocalDate.of(2020, 1, 1)) - .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) - .keywords(List.of("keyword1", "keyword2")) - .publisherHomepage("publisherHomepage") - .dataSource(dataSource) - .customJsonAsString("{\"test\":\"value\"}") - .customJsonLdAsString(""" - { - "https://string": "value", - "https://number": 3.14, - "https://array": [1,2,3], - "https://object": { "https://key": "value" }, - "https://booleans/are/not/supported/by/Eclipse/EDC": true, - "https://null/will/be/eliminated": null - } - """) - .privateCustomJsonAsString("{\"private test\":\"private value\"}") - .privateCustomJsonLdAsString(""" - { - "https://private/string": "value", - "https://private/number": 3.14, - "https://private/array": [1,2,3], - "https://private/object": { "https://key": "value" }, - "https://private/booleans/are/not/supported/by/Eclipse/EDC": true, - "https://private/null/will/be/eliminated": null - } - """) - .build(); + .id("asset-31") + .title("AssetTitle") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .sovereignLegalName("my sovereign") + .geoLocation("40.0, 40.0") + .nutsLocations(Arrays.asList("DE", "DE929")) + .dataSampleUrls(Arrays.asList("https://sample-a", "https://sample-b")) + .referenceFileUrls(Arrays.asList("https://reference-a", "https://reference-b")) + .referenceFilesDescription("RF Description") + .conditionsForUse("Conditions for use") + .dataUpdateFrequency("every month") + .temporalCoverageFrom(LocalDate.of(2020, 1, 1)) + .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataSource(dataSource) + .customJsonAsString(""" + { "test" : "value" } + """) + .customJsonLdAsString(""" + { + "https://string": "value", + "https://number": 3.14, + "https://array": [1,2,3], + "https://object": { "https://key": "value" } + } + """) + .privateCustomJsonAsString(""" + { "private test" : "private value" } + """) + .privateCustomJsonLdAsString(""" + { + "https://private/string": "value", + "https://private/number": 3.14, + "https://private/array": [1,2,3], + "https://private/object": { "https://key": "value" } + } + """) + .build(); // act var response = client.uiApi().createAsset(uiAssetRequest); // assert - assertThat(response.getId()).isEqualTo("asset-1"); + assertThat(response.getId()).isEqualTo("asset-31"); var assets = client.uiApi().getAssetPage().getAssets(); assertThat(assets).hasSize(1); var asset = assets.get(0); - assertThat(asset.getAssetId()).isEqualTo("asset-1"); + assertThat(asset.getAssetId()).isEqualTo("asset-31"); assertThat(asset.getTitle()).isEqualTo("AssetTitle"); assertThat(asset.getDescription()).isEqualTo("AssetDescription"); assertThat(asset.getVersion()).isEqualTo("1.0.0"); @@ -192,41 +211,43 @@ void testAssetCreation(AssetService assetService) { assertThat(asset.getTemporalCoverageToInclusive()).isEqualTo(LocalDate.of(2020, 1, 8)); assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url"); assertThat(asset.getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); - assertThat(asset.getCreatorOrganizationName()).isEqualTo("My Org"); + assertThat(asset.getCreatorOrganizationName()).isEqualTo("Curator Name MyEDC"); assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage"); assertThat(asset.getHttpDatasourceHintsProxyMethod()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyPath()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyBody()).isTrue(); assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" - { "test": "value" } - """); + { "test": "value" } + """); assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" - { - "https://string": "value", - "https://number": 3.14, - "https://array": [1.0, 2.0, 3.0], - "https://object": { "https://key": "value" } - } - """); + { + "https://string": "value", + "https://number": 3.14, + "https://array": [1.0, 2.0, 3.0], + "https://object": { "https://key": "value" } + } + """); assertThatJson(asset.getPrivateCustomJsonAsString()).isEqualTo(""" - { "private test": "private value" } - """); + { "private test": "private value" } + """); assertThatJson(asset.getPrivateCustomJsonLdAsString()).isEqualTo(""" - { - "https://private/string": "value", - "https://private/number": 3.14, - "https://private/array": [1.0, 2.0, 3.0], - "https://private/object": { "https://key": "value" } - } - """); + { + "https://private/string": "value", + "https://private/number": 3.14, + "https://private/array": [1.0, 2.0, 3.0], + "https://private/object": { "https://key": "value" } + } + """); var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); assertThat(assetWithDataAddress.getDataAddress().getProperties()).containsEntry("oauth2:tokenUrl", "https://token-url"); } @Test - void testeditAsset(AssetService assetService) { + void testeditAsset() { + val assetService = providerExtension.getEdcRuntimeExtension().getContext().getService(AssetService.class); + // arrange var dataSource = UiDataSource.builder() .type(DataSourceType.HTTP_DATA) @@ -240,41 +261,41 @@ void testeditAsset(AssetService assetService) { .customProperties(Map.of("oauth2:tokenUrl", "https://token-url")) .build(); var createRequest = UiAssetCreateRequest.builder() - .id("asset-1") - .title("AssetTitle") - .description("AssetDescription") - .licenseUrl("https://license-url") - .version("1.0.0") - .language("en") - .mediaType("application/json") - .dataCategory("dataCategory") - .dataSubcategory("dataSubcategory") - .dataModel("dataModel") - .geoReferenceMethod("geoReferenceMethod") - .transportMode("transportMode") - .sovereignLegalName("my sovereign") - .geoLocation("40.0, 40.0") - .nutsLocations(Arrays.asList("DE", "DE929")) - .dataSampleUrls(Arrays.asList("https://sample-a", "https://sample-b")) - .referenceFileUrls(Arrays.asList("https://reference-a", "https://reference-b")) - .referenceFilesDescription("RF Description") - .conditionsForUse("Conditions for use") - .dataUpdateFrequency("every month") - .temporalCoverageFrom(LocalDate.of(2020, 1, 1)) - .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) - .keywords(List.of("keyword1", "keyword2")) - .publisherHomepage("publisherHomepage") - .dataSource(dataSource) - .customJsonAsString(""" - { "test": "value" } - """) - .customJsonLdAsString(""" - { - "https://to-change": "value1", - "https://for-deletion": "value2" - } - """) - .build(); + .id("asset-41") + .title("AssetTitle") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .sovereignLegalName("my sovereign") + .geoLocation("40.0, 40.0") + .nutsLocations(Arrays.asList("DE", "DE929")) + .dataSampleUrls(Arrays.asList("https://sample-a", "https://sample-b")) + .referenceFileUrls(Arrays.asList("https://reference-a", "https://reference-b")) + .referenceFilesDescription("RF Description") + .conditionsForUse("Conditions for use") + .dataUpdateFrequency("every month") + .temporalCoverageFrom(LocalDate.of(2020, 1, 1)) + .temporalCoverageToInclusive(LocalDate.of(2020, 1, 8)) + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataSource(dataSource) + .customJsonAsString(""" + { "test": "value" } + """) + .customJsonLdAsString(""" + { + "https://to-change": "value1", + "https://for-deletion": "value2" + } + """) + .build(); client.uiApi().createAsset(createRequest); var dataAddressBeforeEdit = assetService.query(QuerySpec.max()) @@ -283,50 +304,50 @@ void testeditAsset(AssetService assetService) { .getProperties(); var editRequest = UiAssetEditRequest.builder() - .title("AssetTitle 2") - .description("AssetDescription 2") - .licenseUrl("https://license-url/2") - .version("2.0.0") - .language("de") - .mediaType("application/json+utf8") - .dataCategory("dataCategory2") - .dataSubcategory("dataSubcategory2") - .dataModel("dataModel2") - .geoReferenceMethod("geoReferenceMethod2") - .sovereignLegalName("my sovereign2") - .geoLocation("50.0, 50.0") - .nutsLocations(Arrays.asList("NL", "NL929")) - .dataSampleUrls(Arrays.asList("https://sample-a2", "https://sample-b2")) - .referenceFileUrls(Arrays.asList("https://reference-a2", "https://reference-b2")) - .referenceFilesDescription("RF Description2") - .conditionsForUse("Conditions for use2") - .dataUpdateFrequency("every week") - .temporalCoverageFrom(LocalDate.of(2021, 1, 1)) - .temporalCoverageToInclusive(LocalDate.of(2021, 1, 8)) - .transportMode("transportMode2") - .keywords(List.of("keyword3")) - .publisherHomepage("publisherHomepage2") - .customJsonAsString(""" - { "edited": "new value" } - """) - .customJsonLdAsString(""" - { - "https://to-change": "new value LD", - "https://for-deletion": null - } - """) - .build(); + .title("AssetTitle 2") + .description("AssetDescription 2") + .licenseUrl("https://license-url/2") + .version("2.0.0") + .language("de") + .mediaType("application/json+utf8") + .dataCategory("dataCategory2") + .dataSubcategory("dataSubcategory2") + .dataModel("dataModel2") + .geoReferenceMethod("geoReferenceMethod2") + .sovereignLegalName("my sovereign2") + .geoLocation("50.0, 50.0") + .nutsLocations(Arrays.asList("NL", "NL929")) + .dataSampleUrls(Arrays.asList("https://sample-a2", "https://sample-b2")) + .referenceFileUrls(Arrays.asList("https://reference-a2", "https://reference-b2")) + .referenceFilesDescription("RF Description2") + .conditionsForUse("Conditions for use2") + .dataUpdateFrequency("every week") + .temporalCoverageFrom(LocalDate.of(2021, 1, 1)) + .temporalCoverageToInclusive(LocalDate.of(2021, 1, 8)) + .transportMode("transportMode2") + .keywords(List.of("keyword3")) + .publisherHomepage("publisherHomepage2") + .customJsonAsString(""" + { "edited": "new value" } + """) + .customJsonLdAsString(""" + { + "https://to-change": "new value LD", + "https://for-deletion": null + } + """) + .build(); // act - var response = client.uiApi().editAsset("asset-1", editRequest); + var response = client.uiApi().editAsset("asset-41", editRequest); // assert - assertThat(response.getId()).isEqualTo("asset-1"); + assertThat(response.getId()).isEqualTo("asset-41"); var assets = client.uiApi().getAssetPage().getAssets(); assertThat(assets).hasSize(1); var asset = assets.get(0); - assertThat(asset.getAssetId()).isEqualTo("asset-1"); + assertThat(asset.getAssetId()).isEqualTo("asset-41"); assertThat(asset.getTitle()).isEqualTo("AssetTitle 2"); assertThat(asset.getDescription()).isEqualTo("AssetDescription 2"); assertThat(asset.getVersion()).isEqualTo("2.0.0"); @@ -349,18 +370,18 @@ void testeditAsset(AssetService assetService) { assertThat(asset.getTemporalCoverageToInclusive()).isEqualTo(LocalDate.of(2021, 1, 8)); assertThat(asset.getLicenseUrl()).isEqualTo("https://license-url/2"); assertThat(asset.getKeywords()).isEqualTo(List.of("keyword3")); - assertThat(asset.getCreatorOrganizationName()).isEqualTo("My Org"); + assertThat(asset.getCreatorOrganizationName()).isEqualTo("Curator Name MyEDC"); assertThat(asset.getPublisherHomepage()).isEqualTo("publisherHomepage2"); assertThat(asset.getHttpDatasourceHintsProxyMethod()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyPath()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyBody()).isTrue(); assertThat(asset.getCustomJsonAsString()).isEqualTo(""" - { "edited": "new value" } - """); + { "edited": "new value" } + """); assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" - { "https://to-change": "new value LD" } - """); + { "https://to-change": "new value LD" } + """); var dataAddressAfterEdit = assetService.query(QuerySpec.max()) .orElseThrow(FailedMappingException::ofFailure).toList().get(0) @@ -379,15 +400,15 @@ void testAssetCreation_noProxying() { .build()) .build(); var uiAssetRequest = UiAssetCreateRequest.builder() - .id("asset-1") - .dataSource(dataSource) - .build(); + .id("asset-51") + .dataSource(dataSource) + .build(); // act var response = client.uiApi().createAsset(uiAssetRequest); // assert - assertThat(response.getId()).isEqualTo("asset-1"); + assertThat(response.getId()).isEqualTo("asset-51"); var assets = client.uiApi().getAssetPage().getAssets(); assertThat(assets).hasSize(1); var asset = assets.get(0); @@ -407,7 +428,7 @@ void testAssetCreation_differentDataAddressType() { )) .build(); var uiAssetRequest = UiAssetCreateRequest.builder() - .id("asset-1") + .id("asset-61") .dataSource(dataSource) .build(); @@ -415,7 +436,7 @@ void testAssetCreation_differentDataAddressType() { var response = client.uiApi().createAsset(uiAssetRequest); // assert - assertThat(response.getId()).isEqualTo("asset-1"); + assertThat(response.getId()).isEqualTo("asset-61"); var assets = client.uiApi().getAssetPage().getAssets(); assertThat(assets).hasSize(1); var asset = assets.get(0); @@ -426,35 +447,37 @@ void testAssetCreation_differentDataAddressType() { } @Test - void testDeleteAsset(AssetService assetService) { + void testDeleteAsset() { + val assetService = providerExtension.getEdcRuntimeExtension().getContext().getService(AssetService.class); + // arrange - createAsset(assetService, "2023-06-01", Map.of(Asset.PROPERTY_ID, "asset-1")); + createAsset(assetService, "2023-06-01", Map.of(Asset.PROPERTY_ID, "asset-71")); assertThat(assetService.query(QuerySpec.max()).getContent()).isNotEmpty(); // act - var response = client.uiApi().deleteAsset("asset-1"); + var response = client.uiApi().deleteAsset("asset-71"); // assert - assertThat(response.getId()).isEqualTo("asset-1"); + assertThat(response.getId()).isEqualTo("asset-71"); assertThat(assetService.query(QuerySpec.max()).getContent()).isEmpty(); } private void createAsset( - AssetService assetService, - String date, - Map properties + AssetService assetService, + String date, + Map properties ) { DataAddress dataAddress = DataAddress.Builder.newInstance() - .type("HttpData") - .property(Prop.Edc.BASE_URL, DATA_SINK) - .build(); + .type("HttpData") + .property(Prop.Edc.BASE_URL, DATA_SINK) + .build(); var asset = Asset.Builder.newInstance() - .id(properties.get(Asset.PROPERTY_ID)) - .properties(edcPropertyUtils.toMapOfObject(properties)) - .dataAddress(dataAddress) - .createdAt(dateFormatterToLong(date)) - .build(); + .id(properties.get(Asset.PROPERTY_ID)) + .properties(edcPropertyUtils.toMapOfObject(properties)) + .dataAddress(dataAddress) + .createdAt(dateFormatterToLong(date)) + .build(); assetService.create(asset); } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java index a4a366355..fa71fe209 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java @@ -26,32 +26,41 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.SneakyThrows; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class CatalogApiTest { - private EdcClient client; - private final String dataOfferId = "my-data-offer-2023-11"; - + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + private final String dataOfferId = "my-data-offer-2023-11"; /** * There used to be issues with the Prop.DISTRIBUTION field being occupied by core EDC. @@ -59,13 +68,14 @@ void setUp(EdcExtension extension) { */ @DisabledOnGithub @Test + @SneakyThrows void testDistributionKey() { // arrange createAsset(); createPolicy(); createContractDefinition(); // act - var catalogPageDataOffers = client.uiApi().getCatalogPageDataOffers(TestUtils.PROTOCOL_ENDPOINT); + var catalogPageDataOffers = client.uiApi().getCatalogPageDataOffers(config.getProtocolEndpoint().getUri().toString()); // assert assertThat(catalogPageDataOffers.size()).isEqualTo(1); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java index 8aca8a06f..9b6536366 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java @@ -18,7 +18,9 @@ import de.sovity.edc.client.gen.model.ContractAgreementDirection; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.utils.jsonld.vocab.Prop; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; @@ -29,19 +31,18 @@ import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.AtomicConstraint; import org.eclipse.edc.policy.model.LiteralExpression; import org.eclipse.edc.policy.model.Operator; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.net.URI; import java.time.LocalDate; @@ -51,33 +52,45 @@ import java.util.Map; import java.util.UUID; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class ContractAgreementPageTest { + private static ConnectorConfig config; + private static ConnectorRemote connector; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("provider", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + connector = new ConnectorRemote(fromConnectorConfig(config)); + return config.getProperties(); + } + ); + private static final int CONTRACT_DEFINITION_ID = 1; private static final String ASSET_ID = UUID.randomUUID().toString(); - EdcClient client; LocalDate today = LocalDate.parse("2019-04-01"); ZonedDateTime todayAsZonedDateTime = today.atStartOfDay(ZoneId.systemDefault()); long todayEpochMillis = todayAsZonedDateTime.toInstant().toEpochMilli(); long todayEpochSeconds = todayAsZonedDateTime.toInstant().getEpochSecond(); - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } - @Test void testContractAgreementPage( - ContractNegotiationStore contractNegotiationStore, - TransferProcessStore transferProcessStore, - AssetIndex assetIndex - ) { + ContractNegotiationStore contractNegotiationStore, + TransferProcessStore transferProcessStore, + AssetIndex assetIndex) { // arrange assetIndex.create(asset(ASSET_ID)).orElseThrow(storeFailure -> new RuntimeException("Failed to create asset")); @@ -116,91 +129,92 @@ void testContractAgreementPage( private DataAddress dataAddress() { return DataAddress.Builder.newInstance() - .type("HttpData") - .properties(Map.of("baseUrl", "http://some-url")) - .build(); + .type("HttpData") + .properties(Map.of("baseUrl", "http://some-url")) + .build(); } private TransferProcess transferProcess(int contract, int transfer, int code) { var dataRequest = DataRequest.Builder.newInstance() - .contractId("my-contract-agreement-" + contract) - .assetId("my-asset-" + contract) - .processId("my-transfer-" + contract + "-" + transfer) - .id("my-data-request-" + contract + "-" + transfer) - .processId("my-transfer-" + contract + "-" + transfer) - .connectorAddress("http://other-connector") - .connectorId("urn:connector:other-connector") - .dataDestination(DataAddress.Builder.newInstance().type("HttpData").build()) - .build(); + .contractId("my-contract-agreement-" + contract) + .assetId("my-asset-" + contract) + .processId("my-transfer-" + contract + "-" + transfer) + .id("my-data-request-" + contract + "-" + transfer) + .processId("my-transfer-" + contract + "-" + transfer) + .connectorAddress("http://other-connector") + .connectorId("urn:connector:other-connector") + .protocol(HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .dataDestination(DataAddress.Builder.newInstance().type("HttpData").build()) + .build(); return TransferProcess.Builder.newInstance() - .id("my-transfer-" + contract + "-" + transfer) - .state(code) - .type(TransferProcess.Type.PROVIDER) - .dataRequest(dataRequest) - .contentDataAddress(DataAddress.Builder.newInstance().type("HttpData").build()) - .errorDetail("my-error-message-" + transfer) - .build(); + .id("my-transfer-" + contract + "-" + transfer) + .state(code) + .type(TransferProcess.Type.PROVIDER) + .dataRequest(dataRequest) + .contentDataAddress(DataAddress.Builder.newInstance().type("HttpData").build()) + .errorDetail("my-error-message-" + transfer) + .build(); } private ContractNegotiation contractDefinition(int contract) { var agreement = ContractAgreement.Builder.newInstance() - .id("my-contract-agreement-" + contract) - .assetId(ASSET_ID) - .contractSigningDate(todayEpochSeconds) - .policy(alwaysTrue()) - .providerId(URI.create("http://other-connector").toString()) - .consumerId(URI.create("http://my-connector").toString()) - .build(); + .id("my-contract-agreement-" + contract) + .assetId(ASSET_ID) + .contractSigningDate(todayEpochSeconds) + .policy(alwaysTrue()) + .providerId(URI.create("http://other-connector").toString()) + .consumerId(URI.create("http://my-connector").toString()) + .build(); // Contract Negotiations can contain multiple Contract Offers (?) // Test this var irrelevantOffer = ContractOffer.Builder.newInstance() - .id("my-contract-offer-" + contract + "-irrelevant") - .assetId(asset(contract + "-irrelevant").getId()) - .policy(alwaysTrue()) - .build(); + .id("my-contract-offer-" + contract + "-irrelevant") + .assetId(asset(contract + "-irrelevant").getId()) + .policy(alwaysTrue()) + .build(); var offer = ContractOffer.Builder.newInstance() - .id("my-contract-offer-" + contract) - .assetId(ASSET_ID) - .policy(alwaysTrue()) - .build(); + .id("my-contract-offer-" + contract) + .assetId(ASSET_ID) + .policy(alwaysTrue()) + .build(); return ContractNegotiation.Builder.newInstance() - .correlationId("my-correlation-" + contract) - .contractAgreement(agreement) - .id("my-contract-negotiation-" + contract) - .counterPartyAddress("http://other-connector") - .counterPartyId("urn:connector:other-connector") - .protocol("ids") - .type(ContractNegotiation.Type.PROVIDER) - .contractOffers(List.of(irrelevantOffer, offer)) - .build(); + .correlationId("my-correlation-" + contract) + .contractAgreement(agreement) + .id("my-contract-negotiation-" + contract) + .counterPartyAddress("http://other-connector") + .counterPartyId("urn:connector:other-connector") + .protocol("ids") + .type(ContractNegotiation.Type.PROVIDER) + .contractOffers(List.of(irrelevantOffer, offer)) + .build(); } private Asset asset(String assetId) { return Asset.Builder.newInstance() - .id(assetId) - .property(Prop.Dcat.LANDING_PAGE, "X") - .createdAt(todayEpochMillis) - .dataAddress(dataAddress()) - .build(); + .id(assetId) + .property(Prop.Dcat.LANDING_PAGE, "X") + .createdAt(todayEpochMillis) + .dataAddress(dataAddress()) + .build(); } private Policy alwaysTrue() { var alwaysTrueConstraint = AtomicConstraint.Builder.newInstance() - .leftExpression(new LiteralExpression("ALWAYS_TRUE")) - .operator(Operator.EQ) - .rightExpression(new LiteralExpression("true")) - .build(); + .leftExpression(new LiteralExpression("ALWAYS_TRUE")) + .operator(Operator.EQ) + .rightExpression(new LiteralExpression("true")) + .build(); var alwaysTruePermission = Permission.Builder.newInstance() - .action(Action.Builder.newInstance().type("USE").build()) - .constraint(alwaysTrueConstraint) - .build(); + .action(Action.Builder.newInstance().type("USE").build()) + .constraint(alwaysTrueConstraint) + .build(); return Policy.Builder.newInstance() - .permission(alwaysTruePermission) - .build(); + .permission(alwaysTruePermission) + .build(); } private String todayPlusDays(int i) { diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java index 284b375ce..84ceac267 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementTransferApiServiceTest.java @@ -17,7 +17,8 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.Json; @@ -28,33 +29,40 @@ import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.Map; import java.util.UUID; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class ContractAgreementTransferApiServiceTest { private static final String DATA_SINK = "http://my-data-sink/api/stuff"; private static final String COUNTER_PARTY_ADDRESS = "http://some-other-connector/api/v1/ids/data"; - EdcClient client; - - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); @Test void startTransferProcessForAgreementId( @@ -110,6 +118,7 @@ void startCustomTransferProcessForAgreementId( .add(Prop.Edc.RECEIVER_HTTP_ENDPOINT, "http://my-pull-backend") .add("this-will-disappear", "because-its-not-an-url") .add("http://unknown/custom-prop", "value")) + .add(Prop.Edc.CTX + "protocol", HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) .build(); var request = new InitiateCustomTransferRequest( contractId, diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java index f2b8cc420..3f9e18eb4 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_definitions/ContractDefinitionPageApiServiceTest.java @@ -7,36 +7,47 @@ import de.sovity.edc.client.gen.model.UiCriterionLiteral; import de.sovity.edc.client.gen.model.UiCriterionLiteralType; import de.sovity.edc.client.gen.model.UiCriterionOperator; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; import org.eclipse.edc.connector.spi.contractdefinition.ContractDefinitionService; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class ContractDefinitionPageApiServiceTest { - // arrange - private EdcClient client; + private static final String PARTICIPANT_ID = "my-edc-participant-id"; - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "edc", + testDatabase -> { + config = forTestDatabase(PARTICIPANT_ID, testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); @Test void contractDefinitionPage(ContractDefinitionService contractDefinitionService) { + // arrange + var criterion = new Criterion("exampleLeft1", "=", "abc"); createContractDefinition(contractDefinitionService, "contractDefinition-id-1", "contractPolicy-id-1", "accessPolicy-id-1", criterion); @@ -62,7 +73,7 @@ void contractDefinitionPage(ContractDefinitionService contractDefinitionService) @Test void contractDefinitionPageSorting(ContractDefinitionService contractDefinitionService) { // arrange - var client = TestUtils.edcClient(); + createContractDefinition( contractDefinitionService, "contractDefinition-id-1", @@ -98,7 +109,7 @@ void contractDefinitionPageSorting(ContractDefinitionService contractDefinitionS @Test void testContractDefinitionCreation(ContractDefinitionService contractDefinitionService) { // arrange - var client = TestUtils.edcClient(); + var criterion = new UiCriterion( "exampleLeft1", UiCriterionOperator.EQ, @@ -133,7 +144,7 @@ void testContractDefinitionCreation(ContractDefinitionService contractDefinition @Test void testDeleteContractDefinition(ContractDefinitionService contractDefinitionService) { // arrange - var client = TestUtils.edcClient(); + var criterion = new Criterion("exampleLeft1", "=", "exampleRight1"); createContractDefinition(contractDefinitionService, "contractDefinition-id-1", "contractPolicy-id-1", "accessPolicy-id-1", criterion); assertThat(contractDefinitionService.query(QuerySpec.max()).getContent().toList()).hasSize(1); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java index 543513525..2bd2af260 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/dashboard/DashboardPageApiServiceTest.java @@ -15,7 +15,8 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.dashboard; import de.sovity.edc.client.EdcClient; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; @@ -35,7 +36,8 @@ import org.eclipse.edc.spi.types.domain.asset.Asset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; import java.util.Collection; import java.util.List; @@ -43,6 +45,7 @@ import java.util.function.Supplier; import java.util.stream.IntStream; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.PROVIDER; @@ -51,46 +54,72 @@ import static org.mockito.Mockito.when; @ApiTest -@ExtendWith(EdcExtension.class) class DashboardPageApiServiceTest { - EdcClient client; + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + + config.setProperty("edc.oauth.token.url", "https://token-url.daps"); + config.setProperty("edc.oauth.provider.jwks.url", "https://jwks-url.daps"); + + config.setProperty("tx.ssi.oauth.token.url", "https://token.miw"); + config.setProperty("tx.ssi.miw.url", "https://miw"); + config.setProperty("tx.ssi.miw.authority.id", "my-authority-id"); + + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); + AssetIndex assetIndex; PolicyDefinitionService policyDefinitionService; TransferProcessService transferProcessService; ContractNegotiationStore contractNegotiationStore; ContractDefinitionService contractDefinitionService; - private Random random; + + private final Random random = new Random(); @BeforeEach - void setUp(EdcExtension extension) { - assetIndex = mock(AssetIndex.class); - extension.registerServiceMock(AssetIndex.class, assetIndex); + void setUp(EdcExtension context) { - policyDefinitionService = mock(PolicyDefinitionService.class); - extension.registerServiceMock(PolicyDefinitionService.class, policyDefinitionService); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); - transferProcessService = mock(TransferProcessService.class); - extension.registerServiceMock(TransferProcessService.class, transferProcessService); - contractNegotiationStore = mock(ContractNegotiationStore.class); - extension.registerServiceMock(ContractNegotiationStore.class, contractNegotiationStore); + assetIndex = mock(); + context.registerServiceMock(AssetIndex.class, assetIndex); - contractDefinitionService = mock(ContractDefinitionService.class); - extension.registerServiceMock(ContractDefinitionService.class, contractDefinitionService); + policyDefinitionService = mock(); + context.registerServiceMock(PolicyDefinitionService.class, policyDefinitionService); - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - random = new Random(); - } + transferProcessService = mock(); + context.registerServiceMock(TransferProcessService.class, transferProcessService); + contractNegotiationStore = mock(); + context.registerServiceMock(ContractNegotiationStore.class, contractNegotiationStore); + + contractDefinitionService = mock(); + context.registerServiceMock(ContractDefinitionService.class, contractDefinitionService); + } @Test void testKpis() { // arrange mockAmounts( - repeat(7, this::mockAsset), - repeat(8, this::mockPolicyDefinition), - repeat(9, this::mockContractDefinition), + repeat(7, Mockito::mock), + repeat(8, Mockito::mock), + repeat(9, Mockito::mock), List.of( mockContractNegotiation(1, CONSUMER), mockContractNegotiation(2, PROVIDER), @@ -135,13 +164,14 @@ void testConnectorMetadata() { // assert assertThat(dashboardPage.getConnectorParticipantId()).isEqualTo("my-edc-participant-id"); - assertThat(dashboardPage.getConnectorDescription()).isEqualTo("My Connector Description"); - assertThat(dashboardPage.getConnectorTitle()).isEqualTo("My Connector"); - assertThat(dashboardPage.getConnectorEndpoint()).isEqualTo(TestUtils.PROTOCOL_ENDPOINT); - assertThat(dashboardPage.getConnectorCuratorName()).isEqualTo("My Org"); - assertThat(dashboardPage.getConnectorCuratorUrl()).isEqualTo("https://connector.my-org"); - assertThat(dashboardPage.getConnectorMaintainerName()).isEqualTo("Maintainer Org"); - assertThat(dashboardPage.getConnectorMaintainerUrl()).isEqualTo("https://maintainer-org"); + assertThat(dashboardPage.getConnectorDescription()).isEqualTo("Connector Description my-edc-participant-id"); + assertThat(dashboardPage.getConnectorTitle()).isEqualTo("Connector Title my-edc-participant-id"); + + assertThat(dashboardPage.getConnectorEndpoint()).isEqualTo(config.getProtocolEndpoint().getUri().toString()); + assertThat(dashboardPage.getConnectorCuratorName()).isEqualTo("Curator Name my-edc-participant-id"); + assertThat(dashboardPage.getConnectorCuratorUrl()).isEqualTo("http://curator.my-edc-participant-id"); + assertThat(dashboardPage.getConnectorMaintainerName()).isEqualTo("Maintainer Name my-edc-participant-id"); + assertThat(dashboardPage.getConnectorMaintainerUrl()).isEqualTo("http://maintainer.my-edc-participant-id"); assertThat(dashboardPage.getConnectorDapsConfig()).isNotNull(); assertThat(dashboardPage.getConnectorDapsConfig().getTokenUrl()).isEqualTo("https://token-url.daps"); @@ -154,15 +184,15 @@ void testConnectorMetadata() { } private Asset mockAsset() { - return mock(Asset.class); + return mock(); } private PolicyDefinition mockPolicyDefinition() { - return mock(PolicyDefinition.class); + return mock(); } private ContractDefinition mockContractDefinition() { - return mock(ContractDefinition.class); + return mock(); } private ContractNegotiation mockContractNegotiation(int contract, ContractNegotiation.Type type) { @@ -183,10 +213,10 @@ private ContractNegotiation mockContractNegotiationInProgress(ContractNegotiatio } private TransferProcess mockTransferProcess(int contractId, int state) { - DataRequest dataRequest = mock(DataRequest.class); + DataRequest dataRequest = mock(); when(dataRequest.getContractId()).thenReturn("ca-" + contractId); - TransferProcess transferProcess = mock(TransferProcess.class); + TransferProcess transferProcess = mock(); when(transferProcess.getId()).thenReturn(String.valueOf(random.nextInt())); when(transferProcess.getDataRequest()).thenReturn(dataRequest); when(transferProcess.getState()).thenReturn(state); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java index 2156b609a..d80ac3cbd 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java @@ -23,41 +23,53 @@ import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.ext.db.jooq.Tables; +import de.sovity.edc.extension.db.directaccess.DslContextFactory; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import lombok.SneakyThrows; +import lombok.val; import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; import org.eclipse.edc.spi.entity.Entity; import org.eclipse.edc.spi.query.QuerySpec; -import org.junit.jupiter.api.BeforeEach; +import org.jooq.DSLContext; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import java.util.Map; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class PolicyDefinitionApiServiceTest { - EdcClient client; + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); UiPolicyConstraint constraint = UiPolicyConstraint.builder() - .left("a") - .operator(OperatorDto.EQ) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value("b") - .build()) - .build(); - - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + .left("a") + .operator(OperatorDto.EQ) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value("b") + .build()) + .build(); @Test void getPolicyList() { @@ -71,8 +83,8 @@ void getPolicyList() { var policyDefinitions = response.getPolicies(); assertThat(policyDefinitions).hasSize(2); var policyDefinition = policyDefinitions.stream() - .filter(it -> it.getPolicyDefinitionId().equals("my-policy-def-1")) - .findFirst().get(); + .filter(it -> it.getPolicyDefinitionId().equals("my-policy-def-1")) + .findFirst().get(); assertThat(policyDefinition.getPolicyDefinitionId()).isEqualTo("my-policy-def-1"); assertThat(policyDefinition.getPolicy().getConstraints()).hasSize(1); @@ -81,23 +93,38 @@ void getPolicyList() { } @Test - void test_sorting(PolicyDefinitionService policyDefinitionService) { + void sortPoliciesFromNewestToOldest(DslContextFactory dslContextFactory) { // arrange - createPolicyDefinition(policyDefinitionService, "my-policy-def-2", 1628956802000L); - createPolicyDefinition(policyDefinitionService, "my-policy-def-0", 1628956800000L); - createPolicyDefinition(policyDefinitionService, "my-policy-def-1", 1628956801000L); + createPolicyDefinition("my-policy-def-0"); + createPolicyDefinition("my-policy-def-1"); + createPolicyDefinition("my-policy-def-2"); + + dslContextFactory.transaction(dsl -> + Map.of( + "my-policy-def-0", 1628956800000L, + "my-policy-def-1", 1628956801000L, + "my-policy-def-2", 1628956802000L + ).forEach((id, time) -> setPolicyDefCreatedAt(dsl, id, time))); // act var result = client.uiApi().getPolicyDefinitionPage(); // assert assertThat(result.getPolicies()) - .extracting(PolicyDefinitionDto::getPolicyDefinitionId) - .containsExactly( - "always-true", - "my-policy-def-2", - "my-policy-def-1", - "my-policy-def-0"); + .extracting(PolicyDefinitionDto::getPolicyDefinitionId) + .containsExactly( + "always-true", + "my-policy-def-2", + "my-policy-def-1", + "my-policy-def-0"); + } + + private static void setPolicyDefCreatedAt(DSLContext dsl, String id, Long time) { + val p = Tables.EDC_POLICYDEFINITIONS; + dsl.update(p) + .set(p.CREATED_AT, time) + .where(p.POLICY_ID.eq(id)) + .execute(); } @Test @@ -105,7 +132,7 @@ void test_delete(PolicyDefinitionService policyDefinitionService) { // arrange createPolicyDefinition("my-policy-def-1"); assertThat(policyDefinitionService.query(QuerySpec.max()).getContent().toList()) - .extracting(Entity::getId).contains("always-true", "my-policy-def-1"); + .extracting(Entity::getId).contains("always-true", "my-policy-def-1"); // act var response = client.uiApi().deletePolicyDefinition("my-policy-def-1"); @@ -113,7 +140,7 @@ void test_delete(PolicyDefinitionService policyDefinitionService) { // assert assertThat(response.getId()).isEqualTo("my-policy-def-1"); assertThat(policyDefinitionService.query(QuerySpec.max()).getContent()) - .extracting(Entity::getId).containsExactly("always-true"); + .extracting(Entity::getId).containsExactly("always-true"); } private void createPolicyDefinition(String policyDefinitionId) { @@ -122,18 +149,4 @@ private void createPolicyDefinition(String policyDefinitionId) { client.uiApi().createPolicyDefinition(policyDefinition); } - @SneakyThrows - private void createPolicyDefinition( - PolicyDefinitionService policyDefinitionService, - String policyDefinitionId, - long createdAt) { - createPolicyDefinition(policyDefinitionId); - var policyDefinition = policyDefinitionService.findById(policyDefinitionId); - - // Forcefully overwrite createdAt - var createdAtField = Entity.class.getDeclaredField("createdAt"); - createdAtField.setAccessible(true); - createdAtField.set(policyDefinition, createdAt); - policyDefinitionService.update(policyDefinition); - } } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java index 88b17cc8b..a0cf4346c 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferHistoryPageApiServiceTest.java @@ -16,32 +16,41 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.ContractAgreementDirection; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.spi.asset.AssetService; import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.text.ParseException; import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createConsumingTransferProcesses; import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createProvidingTransferProcesses; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class TransferHistoryPageApiServiceTest { - EdcClient client; - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); @Test void transferHistoryTest( diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java index 65ba76323..f18f73f05 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessAssetApiServiceTest.java @@ -15,33 +15,41 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; import de.sovity.edc.client.EdcClient; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.spi.asset.AssetService; import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.spi.monitor.Monitor; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.text.ParseException; import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createConsumingTransferProcesses; import static de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessTestUtils.createProvidingTransferProcesses; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class TransferProcessAssetApiServiceTest { - EdcClient client = TestUtils.edcClient(); + private static ConnectorConfig config; + private static EdcClient client; - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); @Test void testProviderTransferProcess( diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java index 70864b626..66ab4ddbf 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/transferhistory/TransferProcessTestUtils.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory; @@ -139,11 +140,13 @@ private static void createTransferProcess( ) throws ParseException { var dataRequestForTransfer = DataRequest.Builder.newInstance() + .id(UUID.randomUUID().toString()) .assetId(assetId) .contractId(contractId) .dataDestination(dataAddress) .connectorAddress(COUNTER_PARTY_ADDRESS) .connectorId(COUNTER_PARTY_ID) + .protocol(HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) .build(); var transferProcess = TransferProcess.Builder.newInstance() diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java index 4fc72cc3f..27c8cafcb 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/KpiApiTest.java @@ -15,25 +15,34 @@ package de.sovity.edc.ext.wrapper.api.usecase; import de.sovity.edc.client.EdcClient; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class KpiApiTest { - EdcClient client; + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } @Test void getKpis() { // act diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java index e411e36a2..a6215b6ad 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java @@ -7,15 +7,14 @@ import de.sovity.edc.client.gen.model.PermissionDto; import de.sovity.edc.client.gen.model.PolicyCreateRequest; import de.sovity.edc.client.gen.model.PolicyDefinitionDto; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.Json; import jakarta.json.JsonObject; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.io.StringReader; import java.util.List; @@ -24,21 +23,30 @@ import static de.sovity.edc.client.gen.model.ExpressionType.AND; import static de.sovity.edc.client.gen.model.ExpressionType.ATOMIC_CONSTRAINT; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @ApiTest -@ExtendWith(EdcExtension.class) public class PolicyDefinitionApiServiceTest { - private EdcClient client; - - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); @Test void createTraceXPolicy() { diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java index 73113dd92..094595f7f 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/SupportedPolicyApiTest.java @@ -15,32 +15,41 @@ package de.sovity.edc.ext.wrapper.api.usecase; import de.sovity.edc.client.EdcClient; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class SupportedPolicyApiTest { - EdcClient edcClient; - @BeforeEach - void setUp(EdcExtension extension) { - TestUtils.setupExtension(extension); - edcClient = TestUtils.edcClient(); - } + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); @Test void supportedPolicies() { // act - var actual = edcClient.useCaseApi().getSupportedFunctions(); + var actual = client.useCaseApi().getSupportedFunctions(); // assert - assertThat(actual).containsExactly("ALWAYS_TRUE", "https://w3id.org/edc/v0.0.1/ns/inForceDate"); + assertThat(actual).contains("ALWAYS_TRUE", "https://w3id.org/edc/v0.0.1/ns/inForceDate"); } } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java index fc92c506e..e8cefe982 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java @@ -31,35 +31,42 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; -import de.sovity.edc.ext.wrapper.TestUtils; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.jsonld.vocab.Prop; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; -import java.util.Map; +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static org.assertj.core.api.Assertions.assertThat; @ApiTest -@ExtendWith(EdcExtension.class) class UseCaseApiWrapperTest { - private EdcClient client; - - private String assetId1 = "test-asset-1"; - private String assetId2 = "test-asset-2"; - private String policyId = "policy-1"; + private static ConnectorConfig config; + private static EdcClient client; + + @RegisterExtension + static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + config = forTestDatabase("my-edc-participant-id", testDatabase); + client = EdcClient.builder() + .managementApiUrl(config.getManagementEndpoint().getUri().toString()) + .managementApiKey(config.getProperties().get("edc.api.auth.key")) + .build(); + return config.getProperties(); + } + ); - @BeforeEach - void setup(EdcExtension extension) { - TestUtils.setupExtension(extension); - client = TestUtils.edcClient(); - } + private static String assetId1 = "test-asset-1"; + private static String assetId2 = "test-asset-2"; + private static String policyId = "policy-1"; @Test @DisabledOnGithub @@ -120,7 +127,7 @@ void shouldFetchWithoutFilterButWithLimit() { private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperator operator, String rightOperand) { return CatalogQuery.builder() - .connectorEndpoint(TestUtils.PROTOCOL_ENDPOINT) + .connectorEndpoint(config.getProtocolEndpoint().getUri().toString()) .filterExpressions( List.of( CatalogFilterExpression.builder() @@ -135,7 +142,7 @@ private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperat private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperator operator, List rightOperand) { return CatalogQuery.builder() - .connectorEndpoint(TestUtils.PROTOCOL_ENDPOINT) + .connectorEndpoint(config.getProtocolEndpoint().getUri().toString()) .filterExpressions( List.of( CatalogFilterExpression.builder() @@ -150,7 +157,7 @@ private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperat private CatalogQuery criterion(Integer limit, Integer offset) { return CatalogQuery.builder() - .connectorEndpoint(TestUtils.PROTOCOL_ENDPOINT) + .connectorEndpoint(config.getProtocolEndpoint().getUri().toString()) .limit(limit) .offset(offset) .build(); @@ -176,7 +183,7 @@ private void setupAssets() { var dataSource = UiDataSource.builder() .type(DataSourceType.HTTP_DATA) .httpData(UiDataSourceHttpData.builder() - .baseUrl(TestUtils.PROTOCOL_ENDPOINT) + .baseUrl(config.getProtocolEndpoint().getUri().toString()) .build()) .build(); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 398d77f68..fa0cdf90c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # groups -edcGroup="org.eclipse.edc" +edcGroup = "org.eclipse.edc" sovityCatalogCrawlerGroup = "de.sovity.edc.catalog.crawler" sovityEdcExtensionGroup = "de.sovity.edc.ext" sovityEdcGroup = "de.sovity.edc" @@ -23,9 +23,11 @@ flywayPlugin = "9.21.1" gson = "2.10.1" gsonFire = "1.8.5" guava = "33.1.0-jre" +hibernateValidator = "8.0.1.Final" hidetakeSwagger = "2.19.2" hikari = "5.0.1" jakartaAnnotation = "1.3.5" +jakartaEl = "4.0.2" jakartaJson = "2.0.1" jakartaRs = "3.1.0" jakartaServletApi = "5.0.0" @@ -43,7 +45,7 @@ jsonUnit = "3.2.7" junit = "5.10.0" loggingHouse = "0.2.10" lombok = "1.18.30" -mockito = "4.8.0" +mockito = "5.12.0" mockserver = "5.15.0" okhttp = "4.12.0" okio = "3.9.0" @@ -87,6 +89,7 @@ edc-boot = { module = "org.eclipse.edc:boot", version.ref = "edc" } edc-configurationFilesystem = { module = "org.eclipse.edc:configuration-filesystem", version.ref = "edc" } edc-connectorCore = { module = "org.eclipse.edc:connector-core", version.ref = "edc" } edc-contractDefinitionApi = { module = "org.eclipse.edc:contract-definition-api", version.ref = "edc" } +edc-contractNegotiationStoreSql = { module = "org.eclipse.edc:contract-negotiation-store-sql", version.ref = "edc" } edc-contractSpi = { module = "org.eclipse.edc:contract-spi", version.ref = "edc" } edc-controlPlaneAggregateServices = { module = "org.eclipse.edc:control-plane-aggregate-services", version.ref = "edc" } edc-controlPlaneCore = { module = "org.eclipse.edc:control-plane-core", version.ref = "edc" } @@ -101,6 +104,7 @@ edc-dataPlaneSelectorCore = { module = "org.eclipse.edc:data-plane-selector-core edc-dataPlaneUtil = { module = "org.eclipse.edc:data-plane-util", version.ref = "edc" } edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-dspApiConfiguration = { module = "org.eclipse.edc:dsp-api-configuration", version.ref = "edc" } +edc-dspNegotiationTransform = { module = "org.eclipse.edc:dsp-negotiation-transform", version.ref = "edc" } edc-dspHttpSpi = { module = "org.eclipse.edc:dsp-http-spi", version.ref = "edc" } edc-dspHttpCore = { module = "org.eclipse.edc:dsp-http-core", version.ref = "edc" } edc-http = { module = "org.eclipse.edc:http", version.ref = "edc" } @@ -116,10 +120,12 @@ edc-oauth2Core = { module = "org.eclipse.edc:oauth2-core", version.ref = "edc" } edc-policyDefinitionApi = { module = "org.eclipse.edc:policy-definition-api", version.ref = "edc" } edc-policyEngineSpi = { module = "org.eclipse.edc:policy-engine-spi", version.ref = "edc" } edc-policyModel = { module = "org.eclipse.edc:policy-model", version.ref = "edc" } +edc-policySpi = { module = "org.eclipse.edc:policy-spi", version.ref = "edc" } edc-sqlCore = { module = "org.eclipse.edc:sql-core", version.ref = "edc" } edc-transactionLocal = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-transferDataPlane = { module = "org.eclipse.edc:transfer-data-plane", version.ref = "edc" } edc-transferProcessApi = { module = "org.eclipse.edc:transfer-process-api", version.ref = "edc" } +edc-transferProcessStoreSql = { module = "org.eclipse.edc:transfer-process-store-sql", version.ref = "edc" } edc-transferSpi = { module = "org.eclipse.edc:transfer-spi", version.ref = "edc" } edc-transformCore = { module = "org.eclipse.edc:transform-core", version.ref = "edc" } edc-transformSpi = { module = "org.eclipse.edc:transform-spi", version.ref = "edc" } @@ -137,9 +143,12 @@ gsonFire = { module = "io.gsonfire:gson-fire", version.ref = "gsonFire" } guava = { module = "com.google.guava:guava", version.ref = "guava" } +hibernate-validation = { module = "org.hibernate.validator:hibernate-validator", version.ref = "hibernateValidator" } + hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } jakarta-annotationApi = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "jakartaAnnotation" } +jakarta-el = { module = "org.glassfish:jakarta.el", version.ref = "jakartaEl" } jakarta-json = { module = "org.glassfish:jakarta.json", version.ref = "jakartaJson" } jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "jakartaRs" } jakarta-servletApi = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakartaServletApi" } diff --git a/settings.gradle.kts b/settings.gradle.kts index bb5b28436..78a0903b2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,8 @@ include(":extensions:catalog-crawler:catalog-crawler") include(":extensions:catalog-crawler:catalog-crawler-db") include(":extensions:catalog-crawler:catalog-crawler-launcher-base") include(":extensions:catalog-crawler:catalog-crawler-e2e-test") +include(":extensions:contract-termination") +include(":extensions:database-direct-access") include(":extensions:edc-ui-config") include(":extensions:last-commit-info") include(":extensions:policy-always-true") @@ -38,6 +40,5 @@ include(":tests") include(":utils:catalog-parser") include(":utils:jooq-database-access") include(":utils:json-and-jsonld-utils") -include(":utils:test-connector-remote") include(":utils:test-utils") include(":utils:versions") diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index afd26a2d6..8b232a5f1 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -6,19 +6,25 @@ dependencies { api(project(":launchers:common:base")) api(project(":launchers:common:auth-mock")) + testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) - testImplementation(project(":utils:test-utils")) + testImplementation(project(":extensions:test-backend-controller")) - testImplementation(project(":utils:test-connector-remote")) testImplementation(project(":extensions:wrapper:clients:java-client")) + testImplementation(project(":utils:test-utils")) testImplementation(libs.jsonUnit.assertj) testImplementation(libs.mockito.core) testImplementation(libs.assertj.core) testImplementation(libs.junit.api) testImplementation(libs.junit.params) testImplementation(libs.mockserver.netty) + testImplementation(libs.restAssured.restAssured) testRuntimeOnly(libs.junit.engine) } +tasks.getByName("test") { + maxParallelForks = Runtime.getRuntime().availableProcessors() / 2 +} + group = libs.versions.sovityEdcGroup.get() diff --git a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java index 1c946456a..436be683b 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.e2e; @@ -82,24 +83,24 @@ class ApiWrapperDemoTest { @BeforeEach void setup() { // set up provider EDC + Client - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); + var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, PROVIDER_DATABASE); providerEdcContext.setConfiguration(providerConfig.getProperties()); providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); providerClient = EdcClient.builder() - .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) - .build(); + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); // set up consumer EDC + Client - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); + var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, CONSUMER_DATABASE); consumerEdcContext.setConfiguration(consumerConfig.getProperties()); consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); consumerClient = EdcClient.builder() - .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) - .build(); + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); @@ -124,89 +125,89 @@ void provide_and_consume() { private void createAsset() { var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl(dataAddress.getDataSourceUrl(dataOfferData)) - .build()) - .build(); + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(dataOfferData)) + .build()) + .build(); var asset = UiAssetCreateRequest.builder() - .id(dataOfferId) - .title("My Data Offer") - .description("Example Data Offer.") - .version("2023-11") - .language("EN") - .publisherHomepage("https://my-department.my-org.com/my-data-offer") - .licenseUrl("https://my-department.my-org.com/my-data-offer#license") - .dataSource(dataSource) - .build(); + .id(dataOfferId) + .title("My Data Offer") + .description("Example Data Offer.") + .version("2023-11") + .language("EN") + .publisherHomepage("https://my-department.my-org.com/my-data-offer") + .licenseUrl("https://my-department.my-org.com/my-data-offer#license") + .dataSource(dataSource) + .build(); providerClient.uiApi().createAsset(asset); } private void createPolicy() { var afterYesterday = UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.GT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(OffsetDateTime.now().minusDays(1).toString()) - .build()) - .build(); + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().minusDays(1).toString()) + .build()) + .build(); var beforeTomorrow = UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.LT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(OffsetDateTime.now().plusDays(1).toString()) - .build()) - .build(); + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.LT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().plusDays(1).toString()) + .build()) + .build(); var policyDefinition = PolicyDefinitionCreateRequest.builder() - .policyDefinitionId(dataOfferId) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(afterYesterday, beforeTomorrow)) - .build()) - .build(); + .policyDefinitionId(dataOfferId) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(afterYesterday, beforeTomorrow)) + .build()) + .build(); providerClient.uiApi().createPolicyDefinition(policyDefinition); } private void createContractDefinition() { var contractDefinition = ContractDefinitionRequest.builder() - .contractDefinitionId(dataOfferId) - .accessPolicyId(dataOfferId) - .contractPolicyId(dataOfferId) - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(dataOfferId) - .build()) - .build())) - .build(); + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); providerClient.uiApi().createContractDefinition(contractDefinition); } private UiContractNegotiation initiateNegotiation(UiDataOffer dataOffer, UiContractOffer contractOffer) { var negotiationRequest = ContractNegotiationRequest.builder() - .counterPartyAddress(dataOffer.getEndpoint()) - .counterPartyParticipantId(dataOffer.getParticipantId()) - .assetId(dataOffer.getAsset().getAssetId()) - .contractOfferId(contractOffer.getContractOfferId()) - .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) - .build(); + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); return consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); } private UiContractNegotiation awaitNegotiationDone(String negotiationId) { var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( - () -> consumerClient.uiApi().getContractNegotiation(negotiationId), - it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS ); assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); @@ -216,9 +217,9 @@ private UiContractNegotiation awaitNegotiationDone(String negotiationId) { private void initiateTransfer(UiContractNegotiation negotiation) { var contractAgreementId = negotiation.getContractAgreementId(); var transferRequest = InitiateTransferRequest.builder() - .contractAgreementId(contractAgreementId) - .dataSinkProperties(dataAddress.getDataSinkProperties()) - .build(); + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataAddress.getDataSinkProperties()) + .build(); consumerClient.uiApi().initiateTransfer(transferRequest); } diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java new file mode 100644 index 000000000..a73735d83 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -0,0 +1,437 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.ApiException; +import de.sovity.edc.client.gen.model.ContractAgreementPage; +import de.sovity.edc.client.gen.model.ContractAgreementPageQuery; +import de.sovity.edc.client.gen.model.ContractTerminatedBy; +import de.sovity.edc.client.gen.model.ContractTerminationRequest; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.TransferHistoryEntry; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import jakarta.ws.rs.HttpMethod; +import lombok.SneakyThrows; +import lombok.val; +import org.awaitility.Awaitility; +import org.eclipse.edc.connector.contract.spi.ContractId; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import java.time.OffsetDateTime; +import java.util.Map; +import java.util.stream.IntStream; + +import static de.sovity.edc.client.gen.model.ContractTerminatedBy.COUNTERPARTY; +import static de.sovity.edc.client.gen.model.ContractTerminatedBy.SELF; +import static de.sovity.edc.client.gen.model.ContractTerminationStatus.ONGOING; +import static de.sovity.edc.client.gen.model.ContractTerminationStatus.TERMINATED; +import static java.time.Duration.ofSeconds; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(E2eTestExtension.class) +public class ContractTerminationTest { + + + @Test + void canGetAgreementPageForNonTerminatedContract( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assets = IntStream.range(0, 3).mapToObj((it) -> scenario.createAsset()); + + val agreements = assets + .peek(scenario::createContractDefinition) + .map(scenario::negotiateAssetAndAwait) + .toList(); + + consumerClient.uiApi().terminateContractAgreement( + agreements.get(0).getContractAgreementId(), + ContractTerminationRequest.builder() + .detail("detail 0") + .reason("reason 0") + .build() + ); + + consumerClient.uiApi().terminateContractAgreement( + agreements.get(1).getContractAgreementId(), + ContractTerminationRequest.builder() + .detail("detail 1") + .reason("reason 1") + .build() + ); + + awaitTerminationCount(consumerClient, 2); + awaitTerminationCount(providerClient, 2); + + // act + // don't terminate the contract + val allAgreements = consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + val terminatedAgreements = + consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().terminationStatus(TERMINATED).build()); + val ongoingAgreements = + consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().terminationStatus(ONGOING).build()); + + // assert + assertThat(allAgreements.getContractAgreements()).hasSize(3); + assertThat(terminatedAgreements.getContractAgreements()).hasSize(2); + assertThat(ongoingAgreements.getContractAgreements()).hasSize(1); + } + + @DisabledOnGithub + @Test + @SneakyThrows + void canGetAgreementPageForTerminatedContract( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + scenario.negotiateAssetAndAwait(assetId); + + val agreements = consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + + // act + + val reason = "Reason"; + val details = "Details"; + consumerClient.uiApi().terminateContractAgreement( + agreements.getContractAgreements().get(0).getContractAgreementId(), + ContractTerminationRequest.builder().reason(reason).detail(details).build()); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + val agreementsAfterTermination = consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + + // assert + assertTermination(agreementsAfterTermination, details, reason, SELF); + } + + @Test + @SneakyThrows + void canTerminateFromConsumer( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient + ) { + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + // act + val detail = "Some detail"; + val reason = "Some reason"; + consumerClient.uiApi().terminateContractAgreement(negotiation.getContractAgreementId(), ContractTerminationRequest.builder() + .detail(detail) + .reason(reason) + .build()); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + val consumerSideAgreements = consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + val providerSideAgreements = providerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + + // assert + assertTermination(consumerSideAgreements, detail, reason, SELF); + assertTermination(providerSideAgreements, detail, reason, COUNTERPARTY); + } + + @DisabledOnGithub + @Test + void limitTheReasonSizeAt100Chars( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + // act + val detail = "Some detail"; + val max = 100; + val maxSize = IntStream.range(0, max).mapToObj(it -> "M").reduce("", (acc, e) -> acc + e); + val tooLong = IntStream.range(0, max + 1).mapToObj(it -> "O").reduce("", (acc, e) -> acc + e); + + // assert when too big + + assertThrows( + ApiException.class, + () -> consumerClient.uiApi() + .terminateContractAgreement(negotiation.getContractAgreementId(), ContractTerminationRequest.builder() + .detail(detail) + .reason(tooLong) + .build()) + ); + + // assert when max size + + consumerClient.uiApi().terminateContractAgreement(negotiation.getContractAgreementId(), ContractTerminationRequest.builder() + .detail(detail) + .reason(maxSize) + .build()); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + // termination completed == success + } + + @Test + void limitTheDetailSizeAt1000Chars( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + // act + val reason = "Some reason"; + val max = 1000; + val maxSize = IntStream.range(0, max).mapToObj(it -> "M").reduce("", (acc, e) -> acc + e); + val tooLong = IntStream.range(0, max + 1).mapToObj(it -> "O").reduce("", (acc, e) -> acc + e); + + // assert when too big + + assertThrows( + ApiException.class, + () -> consumerClient.uiApi().terminateContractAgreement( + negotiation.getContractAgreementId(), + ContractTerminationRequest.builder() + .detail(tooLong) + .reason(reason) + .build()) + ); + + // assert when max size + + consumerClient.uiApi().terminateContractAgreement( + negotiation.getContractAgreementId(), + ContractTerminationRequest.builder() + .detail(maxSize) + .reason(reason) + .build()); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + // termination completed == success + } + + @Test + @SneakyThrows + void canTerminateFromProvider( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + // act + val detail = "Some detail"; + val reason = "Some reason"; + + providerClient.uiApi().terminateContractAgreement( + negotiation.getContractAgreementId(), + ContractTerminationRequest.builder() + .detail(detail) + .reason(reason) + .build()); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + val consumerSideAgreements = consumerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + val providerSideAgreements = providerClient.uiApi().getContractAgreementPage(ContractAgreementPageQuery.builder().build()); + + // assert + + // assert + assertTermination(consumerSideAgreements, detail, reason, COUNTERPARTY); + assertTermination(providerSideAgreements, detail, reason, SELF); + } + + @Test + void doesntCrashWhenAgreementDoesntExist( + @Consumer EdcClient consumerClient) { + // act + assertThrows( + ApiException.class, + () -> consumerClient.uiApi().terminateContractAgreement( + ContractId.create("definition-1", "asset-1").toString(), + ContractTerminationRequest.builder().detail("Some detail").reason("Some reason").build())); + } + + @DisabledOnGithub + @Test + @SneakyThrows + void cantTransferDataAfterTerminated( + E2eScenario scenario, + ClientAndServer mockServer, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assetId = "asset-1"; + val mockedAsset = scenario.createAssetWithMockResource(assetId); + scenario.createContractDefinition(assetId); + scenario.negotiateAssetAndAwait(assetId); + + val destinationPath = "/destination/some/path/"; + val destinationUrl = "http://localhost:" + mockServer.getPort() + destinationPath; + mockServer.when(HttpRequest.request(destinationPath).withMethod("POST")).respond(it -> HttpResponse.response().withStatusCode(200)); + + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + val transferRequest = InitiateTransferRequest.builder() + .contractAgreementId(negotiation.getContractAgreementId()) + .dataSinkProperties( + Map.of( + EDC_NAMESPACE + "baseUrl", destinationUrl, + EDC_NAMESPACE + "method", HttpMethod.POST, + EDC_NAMESPACE + "type", "HttpData" + ) + ) + .build(); + + val transferId = scenario.transferAndAwait(transferRequest); + + val historyEntry = consumerClient.uiApi() + .getTransferHistoryPage() + .getTransferEntries() + .stream() + .filter(it -> it.getTransferProcessId().equals(transferId)) + .findFirst() + .get(); + + assertThat(historyEntry.getState().getCode()).isEqualTo(TransferProcessStates.COMPLETED.code()); + assertThat(mockedAsset.networkAccesses().get()).isGreaterThan(0); + + mockedAsset.networkAccesses().set(0); + + val detail = "Some detail"; + val reason = "Some reason"; + val contractTerminationRequest = ContractTerminationRequest.builder().detail(detail).reason(reason).build(); + val contractAgreementId = negotiation.getContractAgreementId(); + + // only testing the provider's cancellation as this is the party that is concerned by data access + providerClient.uiApi().terminateContractAgreement(contractAgreementId, contractTerminationRequest); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + // act + consumerClient.uiApi().initiateTransfer(transferRequest); + // first transfer attempt + Thread.sleep(10_000); + assertThat(mockedAsset.networkAccesses().get()).isEqualTo(0); + // second transfer attempt + Thread.sleep(10_000); + assertThat(mockedAsset.networkAccesses().get()).isEqualTo(0); + } + + @Test + void canTerminateOnlyOnce( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient) { + + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + val detail = "Some detail"; + val reason = "Some reason"; + val contractTerminationRequest = ContractTerminationRequest.builder().detail(detail).reason(reason).build(); + val contractAgreementId = negotiation.getContractAgreementId(); + val firstTermination = consumerClient.uiApi().terminateContractAgreement(contractAgreementId, contractTerminationRequest); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + // act + + val alreadyExists = consumerClient.uiApi().terminateContractAgreement(contractAgreementId, contractTerminationRequest); + + assertThat(alreadyExists.getLastUpdatedDate()).isEqualTo(firstTermination.getLastUpdatedDate()); + } + + private static void assertTermination( + ContractAgreementPage consumerSideAgreements, + String detail, + String reason, + ContractTerminatedBy terminatedBy) { + + val contractAgreements = consumerSideAgreements.getContractAgreements(); + assertThat(contractAgreements).hasSize(1); + assertThat(contractAgreements.get(0).getTerminationStatus()).isEqualTo(TERMINATED); + + val consumerInformation = contractAgreements.get(0).getTerminationInformation(); + + assertThat(consumerInformation).isNotNull(); + + val now = OffsetDateTime.now(); + assertThat(consumerInformation.getTerminatedAt()).isBetween(now.minusMinutes(1), now.plusMinutes(1)); + + assertThat(consumerInformation.getDetail()).isEqualTo(detail); + assertThat(consumerInformation.getReason()).isEqualTo(reason); + assertThat(consumerInformation.getTerminatedBy()).isEqualTo(terminatedBy); + } + + private TransferHistoryEntry awaitTransfer(EdcClient client, String transferProcessId) { + val historyEntry = Awaitility.await().atMost(10, SECONDS).until(() -> + client.uiApi() + .getTransferHistoryPage() + .getTransferEntries() + .stream() + .filter(it -> it.getTransferProcessId().equals(transferProcessId)) + .findFirst(), + first -> first.map(it -> it.getState().getCode().equals(TransferProcessStates.COMPLETED.code())) + .orElse(false)); + + return historyEntry.get(); + } + + private void awaitTerminationCount(EdcClient client, int count) { + Awaitility.await().atMost(ofSeconds(5)).until( + () -> client.uiApi() + .getContractAgreementPage(ContractAgreementPageQuery.builder().build()) + .getContractAgreements() + .stream() + .filter(it -> it.getTerminationStatus().equals(TERMINATED)) + .count() >= count + ); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java index c8c5f453f..9df67d5d7 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.e2e; @@ -34,8 +35,12 @@ import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseFactory; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.Json; @@ -44,13 +49,12 @@ import lombok.val; import okhttp3.HttpUrl; import org.awaitility.Awaitility; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @@ -67,13 +71,12 @@ import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.stream.Stream; import javax.annotation.Nullable; import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.OK; -import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.RUNNING; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static java.time.Duration.ofSeconds; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.BODY; @@ -87,44 +90,24 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.stop.Stop.stopQuietly; +@ExtendWith(E2eTestExtension.class) class DataSourceParameterizationTest { - private static final String PROVIDER_PARTICIPANT_ID = "provider"; - private static final String CONSUMER_PARTICIPANT_ID = "consumer"; - - @RegisterExtension - static final EdcExtension PROVIDER_EDC_CONTEXT = new EdcExtension(); - @RegisterExtension - static final EdcExtension CONSUMER_EDC_CONTEXT = new EdcExtension(); - - @RegisterExtension - static final TestDatabase PROVIDER_DATABASE = TestDatabaseFactory.getTestDatabase(1); - @RegisterExtension - static final TestDatabase CONSUMER_DATABASE = TestDatabaseFactory.getTestDatabase(2); - - private ConnectorRemote providerConnector; - private ConnectorRemote consumerConnector; - - private EdcClient providerClient; - private EdcClient consumerClient; - private final int port = getFreePort(); private final String sourcePath = "/source/some/path/"; private final String destinationPath = "/destination/some/path/"; - private final String sourceUrl = "http://localhost:" + port + sourcePath; - private final String destinationUrl = "http://localhost:" + port + destinationPath; private ClientAndServer mockServer; private static final AtomicInteger DATA_OFFER_INDEX = new AtomicInteger(0); record TestCase( - String name, - String id, - String method, - @Nullable String body, - @Nullable String mediaType, - @Nullable String path, - Map> queryParams + String name, + String id, + String method, + @Nullable String body, + @Nullable String mediaType, + @Nullable String path, + Map> queryParams ) { @Override public String toString() { @@ -142,217 +125,236 @@ public void stopServer() { stopQuietly(mockServer); } - @BeforeEach - void setup() { - // set up provider EDC + Client - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); - PROVIDER_EDC_CONTEXT.setConfiguration(providerConfig.getProperties()); - providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); - - providerClient = EdcClient.builder() - .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) - .build(); - - // set up consumer EDC + Client - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); - CONSUMER_EDC_CONTEXT.setConfiguration(consumerConfig.getProperties()); - consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); - - consumerClient = EdcClient.builder() - .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) - .build(); - } - @Test - void canUseTheWorkaroundInCustomTransferRequest() { + void canUseTheWorkaroundInCustomTransferRequest( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider ConnectorConfig providerConfig, + @Provider EdcClient providerClient + ) { // arrange val testCase = new TestCase( - "", - "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), - HttpMethod.PATCH, - "[]", - "application/json", - "my-endpoint", - Map.of("filter", List.of("a", "b", "c")) + "", + "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), + HttpMethod.PATCH, + "[]", + "application/json", + "my-endpoint", + Map.of("filter", List.of("a", "b", "c")) ); val received = new AtomicBoolean(false); - prepareDataTransferBackends(testCase, () -> received.set(true)); - - createData(testCase); + withMockServer((mockServer, context) -> { + prepareDataTransferBackends(testCase, () -> received.set(true), mockServer); - // act - val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); - val startNegotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); - val negotiation = awaitNegotiationDone(startNegotiation.getContractNegotiationId()); + createData(providerClient, testCase, context); - String standardBase = "https://w3id.org/edc/v0.0.1/ns/"; - String workaroundBase = "https://sovity.de/workaround/proxy/param/"; - var transferRequestJsonLd = Json.createObjectBuilder() + // act + val providerEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerEndpoint); + val startNegotiation = initiateNegotiation(consumerClient, dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); + val negotiation = awaitNegotiationDone(consumerClient, startNegotiation.getContractNegotiationId()); + + val standardBase = "https://w3id.org/edc/v0.0.1/ns/"; + val workaroundBase = "https://sovity.de/workaround/proxy/param/"; + var transferRequestJsonLd = Json.createObjectBuilder() .add( - Prop.Edc.DATA_DESTINATION, - Json.createObjectBuilder(Map.of( - standardBase + "type", "HttpData", - standardBase + "baseUrl", destinationUrl, - standardBase + "method", "PUT", - workaroundBase + "pathSegments", testCase.path, - workaroundBase + "method", testCase.method, - workaroundBase + "queryParams", "filter=a&filter=b&filter=c", - workaroundBase + "mediaType", testCase.mediaType, - workaroundBase + "body", testCase.body - )).build() + Prop.Edc.DATA_DESTINATION, + Json.createObjectBuilder(Map.of( + standardBase + "type", "HttpData", + standardBase + "baseUrl", context.destinationUrl, + standardBase + "method", "PUT", + workaroundBase + "pathSegments", testCase.path, + workaroundBase + "method", testCase.method, + workaroundBase + "queryParams", "filter=a&filter=b&filter=c", + workaroundBase + "mediaType", testCase.mediaType, + workaroundBase + "body", testCase.body + )).build() ) .add(Prop.Edc.CTX + "transferType", Json.createObjectBuilder() - .add(Prop.Edc.CTX + "contentType", "application/octet-stream") - .add(Prop.Edc.CTX + "isFinite", true) + .add(Prop.Edc.CTX + "contentType", "application/octet-stream") + .add(Prop.Edc.CTX + "isFinite", true) ) .add(Prop.Edc.CTX + "protocol", HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) .add(Prop.Edc.CTX + "managedResources", false) .build(); - var transferRequest = InitiateCustomTransferRequest.builder() + var transferRequest = InitiateCustomTransferRequest.builder() .contractAgreementId(negotiation.getContractAgreementId()) .transferProcessRequestJsonLd(JsonUtils.toJson(transferRequestJsonLd)) .build(); - val transferId = consumerClient.uiApi().initiateCustomTransfer(transferRequest).getId(); + val transferId = scenario.transferAndAwait(transferRequest); + + // assert + val actual = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); + assertThat(actual.getAssetId()).isEqualTo(testCase.id); + assertThat(actual.getTransferProcessId()).isEqualTo(transferId); + assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + + assertThat(received.get()).isTrue(); + }); + } + + private record Context( + int port, + String sourceUrl, + String destinationUrl + ) { + } - awaitTransferCompletion(transferId); + private void withMockServer(BiConsumer consumer) { + int port = getFreePort(); + val mockServer = ClientAndServer.startClientAndServer(port); - // assert - TransferHistoryEntry actual = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); - assertThat(actual.getAssetId()).isEqualTo(testCase.id); - assertThat(actual.getTransferProcessId()).isEqualTo(transferId); - assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + val sourceUrl = "http://localhost:" + port + sourcePath; + val destinationUrl = "http://localhost:" + port + destinationPath; - assertThat(received.get()).isTrue(); + consumer.accept(mockServer, new Context(port, sourceUrl, destinationUrl)); + stopQuietly(mockServer); } - private void createData(TestCase testCase) { - createPolicy(testCase); - createAssetWithParameterizedMethod(testCase); - createContractDefinition(testCase); + private void createData(EdcClient providerClient, TestCase testCase, Context context) { + createPolicy(providerClient, testCase); + createAssetWithParameterizedMethod(providerClient, testCase, context); + createContractDefinition(providerClient, testCase); } @Test - void sendWithEdcManagementApi() { + void sendWithEdcManagementApi( + E2eScenario scenario, + @Consumer ConnectorRemote consumerConnector, + @Consumer EdcClient consumerClient, + @Provider ConnectorConfig providerConfig, + @Provider EdcClient providerClient + ) { // arrange val testCase = new TestCase( - "", - "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), - HttpMethod.PATCH, - "[]", - "application/json", - "my-endpoint", - Map.of("filter", List.of("a", "b", "c")) + "", + "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), + HttpMethod.PATCH, + "[]", + "application/json", + "my-endpoint", + Map.of("filter", List.of("a", "b", "c")) ); val received = new AtomicBoolean(false); - prepareDataTransferBackends(testCase, () -> received.set(true)); - - createData(testCase); + withMockServer((mockServer, context) -> { + prepareDataTransferBackends(testCase, () -> received.set(true), mockServer); - // act - val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); - val startNegotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); - val negotiation = awaitNegotiationDone(startNegotiation.getContractNegotiationId()); + createData(providerClient, testCase, context); - String workaroundBase = "https://sovity.de/workaround/proxy/param/"; - String standardBase = "https://w3id.org/edc/v0.0.1/ns/"; - val transferId = consumerConnector.initiateTransfer( + // act + val providerEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerEndpoint); + val startNegotiation = initiateNegotiation(consumerClient, dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); + val negotiation = awaitNegotiationDone(consumerClient, startNegotiation.getContractNegotiationId()); + + String workaroundBase = "https://sovity.de/workaround/proxy/param/"; + String standardBase = "https://w3id.org/edc/v0.0.1/ns/"; + val transferId = consumerConnector.initiateTransfer( negotiation.getContractAgreementId(), testCase.id, - URI.create("http://localhost:21003/api/dsp"), + URI.create(providerEndpoint), Json.createObjectBuilder(Map.of( - standardBase + "type", "HttpData", - standardBase + "baseUrl", destinationUrl, - standardBase + "method", "PUT", - workaroundBase + "pathSegments", testCase.path, - workaroundBase + "method", testCase.method, - workaroundBase + "queryParams", "filter=a&filter=b&filter=c", - workaroundBase + "mediaType", testCase.mediaType, - workaroundBase + "body", testCase.body + standardBase + "type", "HttpData", + standardBase + "baseUrl", context.destinationUrl, + standardBase + "method", "PUT", + workaroundBase + "pathSegments", testCase.path, + workaroundBase + "method", testCase.method, + workaroundBase + "queryParams", "filter=a&filter=b&filter=c", + workaroundBase + "mediaType", testCase.mediaType, + workaroundBase + "body", testCase.body )).build() - ); + ); - awaitTransferCompletion(transferId); + scenario.awaitTransferCompletion(transferId); - // assert - TransferHistoryEntry actual = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); - assertThat(actual.getAssetId()).isEqualTo(testCase.id); - assertThat(actual.getTransferProcessId()).isEqualTo(transferId); - assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + // assert + TransferHistoryEntry actual = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); + assertThat(actual.getAssetId()).isEqualTo(testCase.id); + assertThat(actual.getTransferProcessId()).isEqualTo(transferId); + assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); - assertThat(received.get()).isTrue(); + assertThat(received.get()).isTrue(); + }); } + @DisabledOnGithub @Test - void canTransferParameterizedAsset() { - source().forEach(testCase -> { + void canTransferParameterizedAsset( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider ConnectorConfig providerConfig, + @Provider EdcClient providerClient) { + + source().parallel().forEach(testCase -> { // arrange val received = new AtomicBoolean(false); - prepareDataTransferBackends(testCase, () -> received.set(true)); + withMockServer((mockServer, context) -> { + prepareDataTransferBackends(testCase, () -> received.set(true), mockServer); - createData(testCase); + createData(providerClient, testCase, context); - // act - val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); - val dataOffer = dataOffers.stream().filter(it -> it.getAsset().getAssetId().equals(testCase.id)).findFirst().get(); - val negotiationInit = initiateNegotiation(dataOffer, dataOffer.getContractOffers().get(0)); - val negotiation = awaitNegotiationDone(negotiationInit.getContractNegotiationId()); - val transferId = initiateTransferWithParameters(negotiation, testCase); + // act + val connectorEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + val dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(connectorEndpoint); + val dataOffer = dataOffers.stream().filter(it -> it.getAsset().getAssetId().equals(testCase.id)).findFirst().get(); + val negotiationInit = initiateNegotiation(consumerClient, dataOffer, dataOffer.getContractOffers().get(0)); + val negotiation = awaitNegotiationDone(consumerClient, negotiationInit.getContractNegotiationId()); + val transferId = initiateTransferWithParameters(consumerClient, negotiation, testCase, context); - awaitTransferCompletion(transferId); + scenario.awaitTransferCompletion(transferId); - // assert - TransferHistoryEntry actual = consumerClient.uiApi() + // assert + TransferHistoryEntry actual = consumerClient.uiApi() .getTransferHistoryPage() .getTransferEntries() .stream() .filter(it -> it.getAssetId().equals(testCase.id)) .findFirst() .get(); - assertThat(actual.getAssetId()).isEqualTo(testCase.id); - assertThat(actual.getTransferProcessId()).isEqualTo(transferId); - assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); + assertThat(actual.getAssetId()).isEqualTo(testCase.id); + assertThat(actual.getTransferProcessId()).isEqualTo(transferId); + assertThat(actual.getState().getSimplifiedState()).isEqualTo(OK); - assertThat(received.get()).isTrue(); + assertThat(received.get()).isTrue(); + }); }); } private Stream source() { val httpMethods = List.of( - HttpMethod.POST, - // HttpMethod.HEAD, - HttpMethod.GET, - HttpMethod.DELETE, - HttpMethod.PUT, - HttpMethod.PATCH, - HttpMethod.OPTIONS + HttpMethod.POST, + // HttpMethod.HEAD, + HttpMethod.GET, + HttpMethod.DELETE, + HttpMethod.PUT, + HttpMethod.PATCH, + HttpMethod.OPTIONS ); val paths = Arrays.asList(null, "different/path/segment"); val queryParameters = List.of( - Map.>of(), - Map.of( - "limit", List.of("10"), - "filter", List.of("a", "b", "c") - ) + Map.>of(), + Map.of( + "limit", List.of("10"), + "filter", List.of("a", "b", "c") + ) ); return httpMethods.stream().flatMap(method -> - getBodyOptionsFor(method).stream().flatMap(body -> - paths.stream().flatMap(usePath -> - queryParameters.stream().map(params -> - new TestCase( - method + " body:" + body + " path:" + usePath + " params=" + params, - "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), - method, - body, - body == null ? null : "application/json", - usePath, - params - ))) - )); + getBodyOptionsFor(method).stream().flatMap(body -> + paths.stream().flatMap(usePath -> + queryParameters.stream().map(params -> + new TestCase( + method + " body:" + body + " path:" + usePath + " params=" + params, + "data-offer-" + DATA_OFFER_INDEX.getAndIncrement(), + method, + body, + body == null ? null : "application/json", + usePath, + params + ))) + )); } @NotNull @@ -370,9 +372,9 @@ private static List getBodyOptionsFor(String method) { return useBodyChoices; } - private void prepareDataTransferBackends(TestCase testCase, Runnable onRequestReceived) { + private void prepareDataTransferBackends(TestCase testCase, Runnable onRequestReceived, ClientAndServer mockServer) { String payload = generateRandomPayload(); - mockServer.reset(); + val requestDefinition = request(sourcePath).withMethod(testCase.method); if (testCase.body != null) { @@ -390,23 +392,23 @@ private void prepareDataTransferBackends(TestCase testCase, Runnable onRequestRe mockServer.when(requestDefinition, once()) - .respond((it) -> new HttpResponse() - .withStatusCode(HttpStatusCode.OK_200.code()) - .withBody(payload, StandardCharsets.UTF_8)); + .respond((it) -> new HttpResponse() + .withStatusCode(HttpStatusCode.OK_200.code()) + .withBody(payload, StandardCharsets.UTF_8)); mockServer.when(request(destinationPath).withMethod(HttpMethod.PUT)) - .respond((HttpRequest httpRequest) -> { - if (new String(httpRequest.getBodyAsRawBytes()).equals(payload)) { - onRequestReceived.run(); - } - return new HttpResponse().withStatusCode(200); - }); + .respond((HttpRequest httpRequest) -> { + if (new String(httpRequest.getBodyAsRawBytes()).equals(payload)) { + onRequestReceived.run(); + } + return new HttpResponse().withStatusCode(200); + }); mockServer.when(request("/.*")) - .respond((HttpRequest httpRequest) -> { - fail("Unexpected network call"); - return new HttpResponse().withStatusCode(HttpStatusCode.GONE_410.code()); - }); + .respond((HttpRequest httpRequest) -> { + fail("Unexpected network call"); + return new HttpResponse().withStatusCode(HttpStatusCode.GONE_410.code()); + }); } private static String generateRandomPayload() { @@ -415,9 +417,9 @@ private static String generateRandomPayload() { return Base64.getEncoder().encodeToString(data); } - private String createAssetWithParameterizedMethod(TestCase testCase) { + private String createAssetWithParameterizedMethod(EdcClient providerClient, TestCase testCase, Context context) { var httpData = new UiDataSourceHttpData(); - httpData.setBaseUrl(sourceUrl); + httpData.setBaseUrl(context.sourceUrl); if (testCase.path != null) { httpData.setEnablePathParameterization(true); } @@ -437,59 +439,59 @@ private String createAssetWithParameterizedMethod(TestCase testCase) { .build(); var asset = UiAssetCreateRequest.builder() - .id(testCase.id) - .title("My Data Offer") - .dataSource(dataSource) - .build(); + .id(testCase.id) + .title("My Data Offer") + .dataSource(dataSource) + .build(); return providerClient.uiApi().createAsset(asset).getId(); } - private void createPolicy(TestCase testCase) { + private void createPolicy(EdcClient providerClient, TestCase testCase) { var policyDefinition = PolicyDefinitionCreateRequest.builder() - .policyDefinitionId(testCase.id) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) - .build()) - .build(); + .policyDefinitionId(testCase.id) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build(); providerClient.uiApi().createPolicyDefinition(policyDefinition); } - private String createContractDefinition(TestCase testCase) { + private String createContractDefinition(EdcClient providerClient, TestCase testCase) { var contractDefinition = ContractDefinitionRequest.builder() - .contractDefinitionId(testCase.id) - .accessPolicyId(testCase.id) - .contractPolicyId(testCase.id) - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(testCase.id) - .build()) - .build())) - .build(); + .contractDefinitionId(testCase.id) + .accessPolicyId(testCase.id) + .contractPolicyId(testCase.id) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(testCase.id) + .build()) + .build())) + .build(); return providerClient.uiApi().createContractDefinition(contractDefinition).getId(); } - private UiContractNegotiation initiateNegotiation(UiDataOffer dataOffer, UiContractOffer contractOffer) { + private UiContractNegotiation initiateNegotiation(EdcClient consumerClient, UiDataOffer dataOffer, UiContractOffer contractOffer) { var negotiationRequest = ContractNegotiationRequest.builder() - .counterPartyAddress(dataOffer.getEndpoint()) - .counterPartyParticipantId(dataOffer.getParticipantId()) - .assetId(dataOffer.getAsset().getAssetId()) - .contractOfferId(contractOffer.getContractOfferId()) - .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) - .build(); + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); return consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); } - private UiContractNegotiation awaitNegotiationDone(String negotiationId) { - var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( - () -> consumerClient.uiApi().getContractNegotiation(negotiationId), - it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + private UiContractNegotiation awaitNegotiationDone(EdcClient consumerClient, String negotiationId) { + var negotiation = Awaitility.await().atMost(ofSeconds(10)).until( + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS ); assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); @@ -497,15 +499,18 @@ private UiContractNegotiation awaitNegotiationDone(String negotiationId) { } private String initiateTransferWithParameters( - UiContractNegotiation negotiation, - TestCase testCase) { + EdcClient consumerClient, + UiContractNegotiation negotiation, + TestCase testCase, + Context context) { + String rootKey = "https://w3id.org/edc/v0.0.1/ns/"; val transferProcessProperties = new HashMap(); var contractAgreementId = negotiation.getContractAgreementId(); Map dataSinkProperties = new HashMap<>(); - dataSinkProperties.put(EDC_NAMESPACE + "baseUrl", destinationUrl); + dataSinkProperties.put(EDC_NAMESPACE + "baseUrl", context.destinationUrl); dataSinkProperties.put(EDC_NAMESPACE + "method", HttpMethod.PUT); dataSinkProperties.put(EDC_NAMESPACE + "type", "HttpData"); transferProcessProperties.put(rootKey + METHOD, testCase.method); @@ -523,8 +528,8 @@ private String initiateTransferWithParameters( if (!testCase.queryParams.isEmpty()) { HttpUrl.Builder builder = new HttpUrl.Builder() - .scheme("http") - .host("example.com"); + .scheme("http") + .host("example.com"); for (val multiValueParam : testCase.queryParams.entrySet()) { for (val singleValue : multiValueParam.getValue()) { @@ -538,28 +543,11 @@ private String initiateTransferWithParameters( } var transferRequest = InitiateTransferRequest.builder() - .contractAgreementId(contractAgreementId) - .dataSinkProperties(dataSinkProperties) - .transferProcessProperties(transferProcessProperties) - .build(); + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataSinkProperties) + .transferProcessProperties(transferProcessProperties) + .build(); return consumerClient.uiApi().initiateTransfer(transferRequest).getId(); } - private String getProtocolEndpoint(ConnectorRemote connector) { - return connector.getConfig().getProtocolEndpoint().getUri().toString(); - } - - private void awaitTransferCompletion(String transferId) { - Awaitility.await().atMost(consumerConnector.timeout).until( - () -> consumerClient.uiApi() - .getTransferHistoryPage() - .getTransferEntries() - .stream() - .filter(it -> it.getTransferProcessId().equals(transferId)) - .findFirst() - .map(it -> it.getState().getSimplifiedState()), - it -> it.orElse(RUNNING) != RUNNING - ); - } - } diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java index 3a47e95e6..76b641959 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java @@ -8,97 +8,42 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; -import de.sovity.edc.client.gen.model.ContractDefinitionRequest; -import de.sovity.edc.client.gen.model.ContractNegotiationRequest; -import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; -import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateTransferRequest; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; -import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; -import de.sovity.edc.client.gen.model.UiContractOffer; -import de.sovity.edc.client.gen.model.UiCriterion; -import de.sovity.edc.client.gen.model.UiCriterionLiteral; -import de.sovity.edc.client.gen.model.UiCriterionLiteralType; -import de.sovity.edc.client.gen.model.UiCriterionOperator; -import de.sovity.edc.client.gen.model.UiDataOffer; -import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; -import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseFactory; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import org.awaitility.Awaitility; -import org.eclipse.edc.junit.extensions.EdcExtension; +import lombok.val; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.HashMap; -import java.util.List; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; -import static org.assertj.core.api.Assertions.assertThat; +@ExtendWith(E2eTestExtension.class) class DataSourceQueryParamsTest { - private static final String PROVIDER_PARTICIPANT_ID = "provider"; - private static final String CONSUMER_PARTICIPANT_ID = "consumer"; - - @RegisterExtension - static EdcExtension providerEdcContext = new EdcExtension(); - @RegisterExtension - static EdcExtension consumerEdcContext = new EdcExtension(); - - @RegisterExtension - static final TestDatabase PROVIDER_DATABASE = TestDatabaseFactory.getTestDatabase(1); - @RegisterExtension - static final TestDatabase CONSUMER_DATABASE = TestDatabaseFactory.getTestDatabase(2); - - private ConnectorRemote providerConnector; - private ConnectorRemote consumerConnector; - - private EdcClient providerClient; - private EdcClient consumerClient; private MockDataAddressRemote dataAddress; private final String encodedParam = "a=%25"; // Unencoded param "a=%" - private final String dataOfferId = "my-data-offer-2023-11"; @BeforeEach - void setup() { - // set up provider EDC + Client - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); - providerEdcContext.setConfiguration(providerConfig.getProperties()); - providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); - - providerClient = EdcClient.builder() - .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) - .build(); - - // set up consumer EDC + Client - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); - consumerEdcContext.setConfiguration(consumerConfig.getProperties()); - consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); - - consumerClient = EdcClient.builder() - .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) - .build(); - + void setup(@Provider ConnectorConfig providerConfig) { // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) - dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); + dataAddress = new MockDataAddressRemote(providerConfig.getDefaultEndpoint()); } @Test @@ -119,101 +64,32 @@ void testDirectQuerying() { */ @DisabledOnGithub @Test - void testQueryParamsDoubleEncoded() { + void testQueryParamsDoubleEncoded(E2eScenario scenario, @Consumer EdcClient consumerClient) { + // arrange - createPolicy(); - createAsset(); - createContractDefinition(); + val assetId = "asset-1"; + scenario.createAsset( + assetId, + UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceQueryParamsUrl()) + .queryString(encodedParam) + .build()); + scenario.createContractDefinition(assetId); // act - var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); - var negotiation = initiateNegotiation(dataOffers.get(0), dataOffers.get(0).getContractOffers().get(0)); - negotiation = awaitNegotiationDone(negotiation.getContractNegotiationId()); - initiateTransfer(negotiation); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + initiateTransfer(consumerClient, negotiation); // assert validateDataTransferred(dataAddress.getDataSinkSpyUrl(), encodedParam); } - private void createAsset() { - var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl(dataAddress.getDataSourceQueryParamsUrl()) - .queryString(encodedParam) - .build()) - .build(); - - var asset = UiAssetCreateRequest.builder() - .id(dataOfferId) - .title("My Data Offer") - .dataSource(dataSource) - .build(); - - providerClient.uiApi().createAsset(asset); - } - - private void createPolicy() { - var policyDefinition = PolicyDefinitionCreateRequest.builder() - .policyDefinitionId(dataOfferId) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) - .build()) - .build(); - - providerClient.uiApi().createPolicyDefinition(policyDefinition); - } - - private void createContractDefinition() { - var contractDefinition = ContractDefinitionRequest.builder() - .contractDefinitionId(dataOfferId) - .accessPolicyId(dataOfferId) - .contractPolicyId(dataOfferId) - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(dataOfferId) - .build()) - .build())) - .build(); - - providerClient.uiApi().createContractDefinition(contractDefinition); - } - - private UiContractNegotiation initiateNegotiation(UiDataOffer dataOffer, UiContractOffer contractOffer) { - var negotiationRequest = ContractNegotiationRequest.builder() - .counterPartyAddress(dataOffer.getEndpoint()) - .counterPartyParticipantId(dataOffer.getParticipantId()) - .assetId(dataOffer.getAsset().getAssetId()) - .contractOfferId(contractOffer.getContractOfferId()) - .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) - .build(); - - return consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); - } - - private UiContractNegotiation awaitNegotiationDone(String negotiationId) { - var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( - () -> consumerClient.uiApi().getContractNegotiation(negotiationId), - it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS - ); - - assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); - return negotiation; - } - - private void initiateTransfer(UiContractNegotiation negotiation) { + private void initiateTransfer(EdcClient consumerClient, UiContractNegotiation negotiation) { var contractAgreementId = negotiation.getContractAgreementId(); var transferRequest = InitiateTransferRequest.builder() - .contractAgreementId(contractAgreementId) - .dataSinkProperties(dataAddress.getDataSinkProperties()) - .build(); + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataAddress.getDataSinkProperties()) + .build(); consumerClient.uiApi().initiateTransfer(transferRequest); } - - private String getProtocolEndpoint(ConnectorRemote connector) { - return connector.getConfig().getProtocolEndpoint().getUri().toString(); - } } diff --git a/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java b/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java index 169ef34c1..08876aaa0 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java @@ -8,72 +8,49 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.e2e; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; -import org.eclipse.edc.junit.extensions.EdcExtension; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.UUID; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +@ExtendWith(E2eTestExtension.class) class ManagementApiTransferTest { - private static final String PROVIDER_PARTICIPANT_ID = "provider"; - private static final String CONSUMER_PARTICIPANT_ID = "consumer"; - private static final String TEST_BACKEND_TEST_DATA = UUID.randomUUID().toString(); - - @RegisterExtension - static EdcExtension providerEdcContext = new EdcExtension(); - @RegisterExtension - static EdcExtension consumerEdcContext = new EdcExtension(); - - @RegisterExtension - static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); - @RegisterExtension - static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); - - private ConnectorRemote providerConnector; - private ConnectorRemote consumerConnector; private MockDataAddressRemote dataAddress; + private static final String TEST_BACKEND_TEST_DATA = UUID.randomUUID().toString(); @BeforeEach - void setup() { - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); - providerEdcContext.setConfiguration(providerConfig.getProperties()); - providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); - - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); - consumerEdcContext.setConfiguration(consumerConfig.getProperties()); - consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); - + void setup(@Provider ConnectorRemote providerConnector) { // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); } @Test - void testDataTransfer() { + void testDataTransfer(@Consumer ConnectorRemote consumerConnector, @Provider ConnectorRemote providerConnector) { // arrange var assetId = UUID.randomUUID().toString(); providerConnector.createDataOffer(assetId, dataAddress.getDataSourceUrl(TEST_BACKEND_TEST_DATA)); // act consumerConnector.consumeOffer( - providerConnector.getParticipantId(), - providerConnector.getConfig().getProtocolEndpoint().getUri(), - assetId, - dataAddress.getDataSinkJsonLd()); + providerConnector.getParticipantId(), + providerConnector.getConfig().getProtocolEndpoint().getUri(), + assetId, + dataAddress.getDataSinkJsonLd()); // assert validateDataTransferred(dataAddress.getDataSinkSpyUrl(), TEST_BACKEND_TEST_DATA); diff --git a/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java b/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java deleted file mode 100644 index dd6d9e7e1..000000000 --- a/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - */ - -package de.sovity.edc.e2e; - -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.client.gen.model.ContractAgreementDirection; -import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; -import de.sovity.edc.ext.wrapper.utils.EdcDateUtils; -import de.sovity.edc.extension.e2e.connector.ConnectorRemote; -import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; -import de.sovity.edc.extension.utils.junit.DisabledOnGithub; -import org.assertj.core.api.SoftAssertions; -import org.assertj.core.data.TemporalUnitLessThanOffset; -import org.eclipse.edc.junit.extensions.EdcExtension; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.nio.file.Paths; -import java.time.OffsetDateTime; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.function.Predicate; - -import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; -import static org.assertj.core.api.Assertions.assertThat; - - -/** - * Test data offers and contracts of an MS8 connector migrated to the current version. - */ -class Ms8ConnectorMigrationTest { - - private static final String PROVIDER_PARTICIPANT_ID = "example-provider"; - private static final String CONSUMER_PARTICIPANT_ID = "example-connector"; - - @RegisterExtension - static EdcExtension providerEdcContext = new EdcExtension(); - @RegisterExtension - static EdcExtension consumerEdcContext = new EdcExtension(); - - @RegisterExtension - static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); - @RegisterExtension - static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); - - private ConnectorRemote providerConnector; - private ConnectorRemote consumerConnector; - private EdcClient providerClient; - private EdcClient consumerClient; - private MockDataAddressRemote dataAddress; - - @BeforeEach - void setup() { - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); - providerConfig.setProperty("edc.flyway.additional.migration.locations", - "filesystem:%s".formatted(getAbsoluteTestResourcePath("db/additional-test-data/provider"))); - providerEdcContext.setConfiguration(providerConfig.getProperties()); - providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); - - providerClient = EdcClient.builder() - .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) - .build(); - - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); - consumerConfig.setProperty("edc.flyway.additional.migration.locations", - "filesystem:%s".formatted(getAbsoluteTestResourcePath("db/additional-test-data/consumer"))); - consumerEdcContext.setConfiguration(consumerConfig.getProperties()); - consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); - - consumerClient = EdcClient.builder() - .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) - .build(); - - // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) - dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); - } - - @DisabledOnGithub - @Test - void testMs8DataOffer_Properties() { - // arrange - var providerEndpoint = endpoint(providerConnector); - - // act - var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerEndpoint); - var asset = first(dataOffers, it -> it.getAsset().getAssetId().equals("first-asset-1.0")).getAsset(); - - // assert - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(asset.getAssetId()).isEqualTo("first-asset-1.0"); - softly.assertThat(asset.getAssetJsonLd()).startsWith("{").endsWith("}"); - softly.assertThat(asset.getCreatorOrganizationName()).isEqualTo("Example GmbH"); - softly.assertThat(asset.getDataCategory()).isEqualTo("Traffic Information"); - softly.assertThat(asset.getDataModel()).isEqualTo("data-model"); - softly.assertThat(asset.getDataSubcategory()).isEqualTo("Accidents"); - softly.assertThat(asset.getDescription()).isEqualTo("My First Asset"); - softly.assertThat(asset.getGeoReferenceMethod()).isEqualTo("geo-ref"); - softly.assertThat(asset.getHttpDatasourceHintsProxyBody()).isFalse(); - softly.assertThat(asset.getHttpDatasourceHintsProxyMethod()).isFalse(); - softly.assertThat(asset.getHttpDatasourceHintsProxyPath()).isFalse(); - softly.assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isFalse(); - softly.assertThat(asset.getKeywords()).containsExactlyInAnyOrder("first", "asset"); - softly.assertThat(asset.getLandingPageUrl()).isEqualTo("https://endpoint-documentation"); - softly.assertThat(asset.getLanguage()).isEqualTo("https://w3id.org/idsa/code/EN"); - softly.assertThat(asset.getLicenseUrl()).isEqualTo("https://standard-license"); - softly.assertThat(asset.getMediaType()).isEqualTo("text/plain"); - softly.assertThat(asset.getTitle()).isEqualTo("First Asset"); - softly.assertThat(asset.getPublisherHomepage()).isEqualTo("https://publisher"); - softly.assertThat(asset.getTransportMode()).isEqualTo("Rail"); - softly.assertThat(asset.getVersion()).isEqualTo("1.0"); - }); - } - - @DisabledOnGithub - @Test - void testMs8ProvidingTransferProcess() { - // arrange - - // act - var providerTransfers = providerClient.uiApi().getTransferHistoryPage().getTransferEntries(); - assertThat(providerTransfers).hasSize(1); - var providerTransfer = providerTransfers.get(0); - - // assert - assertThat(providerTransfer.getAssetId()).isEqualTo("first-asset-1.0"); - assertThat(providerTransfer.getAssetName()).isEqualTo("First Asset"); - assertThat(providerTransfer.getContractAgreementId()).isEqualTo("Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi"); - assertThat(providerTransfer.getCounterPartyConnectorEndpoint()).isEqualTo(endpoint(consumerConnector)); - assertThat(providerTransfer.getCounterPartyParticipantId()).isEqualTo(consumerConnector.getParticipantId()); - assertIsEqualOffsetDateTime(providerTransfer.getCreatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208010855L)); - assertThat(providerTransfer.getDirection()).isEqualTo(ContractAgreementDirection.PROVIDING); - assertThat(providerTransfer.getErrorMessage()).isNull(); - assertIsEqualOffsetDateTime(providerTransfer.getLastUpdatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208010083L)); - assertThat(providerTransfer.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); - assertThat(providerTransfer.getTransferProcessId()).isEqualTo("27075fc4-b18f-44e1-8bde-a9f62817dab2"); - } - - private void assertIsEqualOffsetDateTime(OffsetDateTime actual, OffsetDateTime expected) { - assertThat(actual).isCloseTo(expected, new TemporalUnitLessThanOffset(1, ChronoUnit.MINUTES)); - } - - @DisabledOnGithub - @Test - void testMs8ConsumingTransferProcess() { - // arrange - - // act - var consumerTransfers = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries(); - assertThat(consumerTransfers).hasSize(1); - var consumerTransfer = consumerTransfers.get(0); - - // assert - assertThat(consumerTransfer.getAssetId()).isEqualTo("first-asset-1.0"); - assertThat(consumerTransfer.getAssetName()).isEqualTo("first-asset-1.0"); - assertThat(consumerTransfer.getContractAgreementId()).isEqualTo("Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi"); - assertThat(consumerTransfer.getCounterPartyConnectorEndpoint()).isEqualTo(endpoint(providerConnector)); - assertThat(consumerTransfer.getCounterPartyParticipantId()).isEqualTo(providerConnector.getParticipantId()); - assertIsEqualOffsetDateTime(consumerTransfer.getCreatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208008652L)); - assertThat(consumerTransfer.getDirection()).isEqualTo(ContractAgreementDirection.CONSUMING); - assertThat(consumerTransfer.getErrorMessage()).isNull(); - assertIsEqualOffsetDateTime(consumerTransfer.getLastUpdatedDate(), EdcDateUtils.utcMillisToOffsetDateTime(1695208011094L)); - assertThat(consumerTransfer.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); - assertThat(consumerTransfer.getTransferProcessId()).isEqualTo("946aadd4-d4bf-47e9-8aea-c2279070e839"); - } - - @Test - void testMs8DataOffer_negotiateAndTransferNewContract() { - // arrange - var assetIds = providerConnector.getAssetIds(); - assertThat(assetIds).contains("second-asset"); - - // act - consumerConnector.consumeOffer( - providerConnector.getParticipantId(), - providerConnector.getConfig().getProtocolEndpoint().getUri(), - "second-asset", - dataAddress.getDataSinkJsonLd()); - - // assert - validateDataTransferred(dataAddress.getDataSinkSpyUrl(), "second-asset-data"); - } - - @Test - void testMs8Contract_transfer() { - // arrange - var assetIds = providerConnector.getAssetIds(); - assertThat(assetIds).contains("second-asset"); - - // act - var transferProcessId = consumerConnector.initiateTransfer( - "Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi", - "first-asset-1.0", - providerConnector.getConfig().getProtocolEndpoint().getUri(), - dataAddress.getDataSinkJsonLd() - ); - - // assert - assertThat(transferProcessId).isNotNull(); - validateDataTransferred(dataAddress.getDataSinkSpyUrl(), "first-asset-data"); - } - - private T first(List items, Predicate predicate) { - return items.stream().filter(predicate).findFirst().get(); - } - - private String endpoint(ConnectorRemote remote) { - return remote.getConfig().getProtocolEndpoint().getUri().toString(); - } - - public String getAbsoluteTestResourcePath(String path) { - return Paths.get("").resolve("src/test/resources").resolve(path).toAbsolutePath().toString(); - } -} diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index 5cad0820f..f16aa4d7b 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.e2e; @@ -40,8 +41,10 @@ import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; @@ -49,7 +52,6 @@ import jakarta.json.JsonObject; import lombok.val; import org.awaitility.Awaitility; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -65,8 +67,6 @@ import static de.sovity.edc.client.gen.model.ContractAgreementDirection.CONSUMING; import static de.sovity.edc.client.gen.model.ContractAgreementDirection.PROVIDING; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -78,49 +78,25 @@ class UiApiWrapperTest { private static final String CONSUMER_PARTICIPANT_ID = "consumer"; @RegisterExtension - static EdcExtension providerEdcContext = new EdcExtension(); - @RegisterExtension - static EdcExtension consumerEdcContext = new EdcExtension(); - - @RegisterExtension - static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); - @RegisterExtension - static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); + private static E2eTestExtension e2eTestExtension = new E2eTestExtension(CONSUMER_PARTICIPANT_ID, PROVIDER_PARTICIPANT_ID); - private ConnectorRemote providerConnector; - private ConnectorRemote consumerConnector; - - private EdcClient providerClient; - private EdcClient consumerClient; private MockDataAddressRemote dataAddress; @BeforeEach - void setup() { - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); - providerEdcContext.setConfiguration(providerConfig.getProperties()); - providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); - - providerClient = EdcClient.builder() - .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) - .build(); - - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); - consumerEdcContext.setConfiguration(consumerConfig.getProperties()); - consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); - - consumerClient = EdcClient.builder() - .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) - .build(); - + void setup(@Provider ConnectorRemote providerConnector) { // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); } @DisabledOnGithub @Test - void provide_consume_assetMapping_policyMapping_agreements() { + void provide_consume_assetMapping_policyMapping_agreements( + @Consumer ConnectorConfig consumerConfig, + @Consumer ConnectorRemote consumerConnector, + @Consumer EdcClient consumerClient, + @Provider ConnectorConfig providerConfig, + @Provider EdcClient providerClient) { + // arrange var data = "expected data 123"; var yesterday = OffsetDateTime.now().minusDays(1); @@ -207,25 +183,26 @@ void provide_consume_assetMapping_policyMapping_agreements() { assertThat(assets).hasSize(1); var asset = assets.get(0); - var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + var providerProtocolEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint); assertThat(dataOffers).hasSize(1); var dataOffer = dataOffers.get(0); assertThat(dataOffer.getContractOffers()).hasSize(1); var contractOffer = dataOffer.getContractOffers().get(0); // act - var negotiation = negotiate(dataOffer, contractOffer); - initiateTransfer(negotiation); + var negotiation = negotiate(consumerClient, consumerConnector, dataOffer, contractOffer); + initiateTransfer(consumerClient, negotiation); var providerAgreements = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements(); var consumerAgreements = consumerClient.uiApi().getContractAgreementPage(null).getContractAgreements(); // assert - assertThat(dataOffer.getEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); + assertThat(dataOffer.getEndpoint()).isEqualTo(providerProtocolEndpoint); assertThat(dataOffer.getParticipantId()).isEqualTo(PROVIDER_PARTICIPANT_ID); assertThat(dataOffer.getAsset().getAssetId()).isEqualTo(assetId); assertThat(dataOffer.getAsset().getTitle()).isEqualTo("AssetName"); - assertThat(dataOffer.getAsset().getConnectorEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); - assertThat(dataOffer.getAsset().getParticipantId()).isEqualTo(providerConnector.getParticipantId()); + assertThat(dataOffer.getAsset().getConnectorEndpoint()).isEqualTo(providerProtocolEndpoint); + assertThat(dataOffer.getAsset().getParticipantId()).isEqualTo(providerConfig.getProperties().get("edc.participant.id")); assertThat(dataOffer.getAsset().getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); assertThat(dataOffer.getAsset().getDescription()).isEqualTo("AssetDescription"); assertThat(dataOffer.getAsset().getVersion()).isEqualTo("1.0.0"); @@ -266,8 +243,8 @@ void provide_consume_assetMapping_policyMapping_agreements() { // while the data offer on the consumer side won't contain private properties, the asset page on the provider side should assertThat(asset.getAssetId()).isEqualTo(assetId); assertThat(asset.getTitle()).isEqualTo("AssetName"); - assertThat(asset.getConnectorEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); - assertThat(asset.getParticipantId()).isEqualTo(providerConnector.getParticipantId()); + assertThat(asset.getConnectorEndpoint()).isEqualTo(providerProtocolEndpoint); + assertThat(asset.getParticipantId()).isEqualTo(providerConfig.getProperties().get("edc.participant.id")); assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" { "test": "value" } @@ -294,7 +271,7 @@ void provide_consume_assetMapping_policyMapping_agreements() { // Provider Contract Agreement assertThat(providerAgreement.getContractAgreementId()).isEqualTo(negotiation.getContractAgreementId()); assertThat(providerAgreement.getDirection()).isEqualTo(PROVIDING); - assertThat(providerAgreement.getCounterPartyAddress()).isEqualTo("http://localhost:23003/api/dsp"); + assertThat(providerAgreement.getCounterPartyAddress()).isEqualTo(consumerConfig.getProtocolEndpoint().getUri().toString()); assertThat(providerAgreement.getCounterPartyId()).isEqualTo(CONSUMER_PARTICIPANT_ID); assertThat(providerAgreement.getAsset().getAssetId()).isEqualTo(assetId); @@ -329,11 +306,11 @@ void provide_consume_assetMapping_policyMapping_agreements() { validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); - validateTransferProcessesOk(); + validateTransferProcessesOk(consumerClient, providerClient); } @Test - void canOverrideTheWellKnowPropertiesUsingTheCustomProperties() { + void canOverrideTheWellKnowPropertiesUsingTheCustomProperties(@Provider EdcClient providerClient) { // arrange var dataSource = UiDataSource.builder() .type(DataSourceType.HTTP_DATA) @@ -379,11 +356,14 @@ void canOverrideTheWellKnowPropertiesUsingTheCustomProperties() { """); } - // TODO throw an error if the id is overridden - @DisabledOnGithub @Test - void customTransferRequest() { + void customTransferRequest( + @Consumer ConnectorRemote consumerConnector, + @Consumer EdcClient consumerClient, + @Provider ConnectorConfig providerConfig, + @Provider EdcClient providerClient) { + // arrange var data = "expected data 123"; @@ -414,14 +394,15 @@ void customTransferRequest() { .assetSelector(List.of()) .build()); - var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + val providerProtocolEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint); assertThat(dataOffers).hasSize(1); var dataOffer = dataOffers.get(0); assertThat(dataOffer.getContractOffers()).hasSize(1); var contractOffer = dataOffer.getContractOffers().get(0); // act - var negotiation = negotiate(dataOffer, contractOffer); + var negotiation = negotiate(consumerClient, consumerConnector, dataOffer, contractOffer); var transferRequestJsonLd = Json.createObjectBuilder() .add( Prop.Edc.DATA_DESTINATION, @@ -445,7 +426,12 @@ void customTransferRequest() { @DisabledOnGithub @Test - void editAssetOnLiveContract() { + void editAssetOnLiveContract( + @Consumer ConnectorRemote consumerConnector, + @Consumer EdcClient consumerClient, + @Provider ConnectorConfig providerConfig, + @Provider EdcClient providerClient) { + // arrange var data = "expected data 123"; @@ -500,12 +486,13 @@ void editAssetOnLiveContract() { .build())) .build()); - var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)); + val providerProtocolEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint); assertThat(dataOffers).hasSize(1); var dataOffer = dataOffers.get(0); assertThat(dataOffer.getContractOffers()).hasSize(1); var contractOffer = dataOffer.getContractOffers().get(0); - var negotiation = negotiate(dataOffer, contractOffer); + var negotiation = negotiate(consumerClient, consumerConnector, dataOffer, contractOffer); // act providerClient.uiApi().editAsset(assetId, UiAssetEditRequest.builder() @@ -537,10 +524,10 @@ void editAssetOnLiveContract() { } """) .build()); - initiateTransfer(negotiation); + initiateTransfer(consumerClient, negotiation); // assert - assertThat(consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); + assertThat(consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); val firstAsset = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements().get(0).getAsset(); assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" @@ -566,11 +553,16 @@ void editAssetOnLiveContract() { } """); validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); - validateTransferProcessesOk(); + validateTransferProcessesOk(consumerClient, providerClient); assertThat(providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0).getAssetName()).isEqualTo("Good Asset Title"); } - private UiContractNegotiation negotiate(UiDataOffer dataOffer, UiContractOffer contractOffer) { + private UiContractNegotiation negotiate( + EdcClient consumerClient, + ConnectorRemote consumerConnector, + UiDataOffer dataOffer, + UiContractOffer contractOffer) { + var negotiationRequest = ContractNegotiationRequest.builder() .counterPartyAddress(dataOffer.getEndpoint()) .counterPartyParticipantId(dataOffer.getParticipantId()) @@ -591,7 +583,7 @@ private UiContractNegotiation negotiate(UiDataOffer dataOffer, UiContractOffer c return negotiation; } - private void initiateTransfer(UiContractNegotiation negotiation) { + private void initiateTransfer(EdcClient consumerClient, UiContractNegotiation negotiation) { var contractAgreementId = negotiation.getContractAgreementId(); var transferRequest = InitiateTransferRequest.builder() .contractAgreementId(contractAgreementId) @@ -600,8 +592,8 @@ private void initiateTransfer(UiContractNegotiation negotiation) { consumerClient.uiApi().initiateTransfer(transferRequest); } - private void validateTransferProcessesOk() { - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + private void validateTransferProcessesOk(EdcClient consumerClient, EdcClient providerClient) { + await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> { var providing = providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); var consuming = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0); assertThat(providing.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); @@ -614,8 +606,4 @@ private JsonObject getDatasinkPropertiesJsonObject() { var props = dataAddress.getDataSinkProperties(); return Json.createObjectBuilder((Map) (Map) props).build(); } - - private String getProtocolEndpoint(ConnectorRemote connector) { - return connector.getConfig().getProtocolEndpoint().getUri().toString(); - } } diff --git a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java index a32f56217..2e8fa6630 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.e2e; @@ -36,82 +37,47 @@ import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; -import de.sovity.edc.extension.e2e.db.TestDatabase; -import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.jsonld.vocab.Prop; -import org.eclipse.edc.junit.extensions.EdcExtension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.ExtendWith; import java.time.OffsetDateTime; import java.util.List; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; import static org.assertj.core.api.Assertions.assertThat; +@ExtendWith(E2eTestExtension.class) class UseCaseApiWrapperTest { - private static final String PROVIDER_PARTICIPANT_ID = "provider"; - private static final String CONSUMER_PARTICIPANT_ID = "consumer"; - - @RegisterExtension - static EdcExtension providerEdcContext = new EdcExtension(); - @RegisterExtension - static EdcExtension consumerEdcContext = new EdcExtension(); - - @RegisterExtension - static final TestDatabase PROVIDER_DATABASE = new TestDatabaseViaTestcontainers(); - @RegisterExtension - static final TestDatabase CONSUMER_DATABASE = new TestDatabaseViaTestcontainers(); - - private ConnectorRemote providerConnector; - private ConnectorRemote consumerConnector; - - private EdcClient providerClient; - private EdcClient consumerClient; private MockDataAddressRemote dataAddress; private final String dataOfferData = "expected data 123"; private final String dataOfferId = "my-data-offer-2023-11"; @BeforeEach - void setup() { - // set up provider EDC + Client - var providerConfig = forTestDatabase(PROVIDER_PARTICIPANT_ID, 21000, PROVIDER_DATABASE); - providerEdcContext.setConfiguration(providerConfig.getProperties()); - providerConnector = new ConnectorRemote(fromConnectorConfig(providerConfig)); - - providerClient = EdcClient.builder() - .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) - .build(); - - // set up consumer EDC + Client - var consumerConfig = forTestDatabase(CONSUMER_PARTICIPANT_ID, 23000, CONSUMER_DATABASE); - consumerEdcContext.setConfiguration(consumerConfig.getProperties()); - consumerConnector = new ConnectorRemote(fromConnectorConfig(consumerConfig)); - - consumerClient = EdcClient.builder() - .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) - .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) - .build(); - + void setup(@Provider ConnectorRemote providerConnector) { // We use the provider EDC as data sink / data source (it has the test-backend-controller extension) dataAddress = new MockDataAddressRemote(providerConnector.getConfig().getDefaultEndpoint()); } @DisabledOnGithub @Test - void catalog_filtering_by_like() { + void catalog_filtering_by_like( + @Consumer EdcClient consumerClient, + @Provider ConnectorRemote providerConnector, + @Provider EdcClient providerClient) { + // arrange - createPolicy(); - createAsset(); - createContractDefinition(); + createPolicy(providerClient); + createAsset(providerClient); + createContractDefinition(providerClient); - var query = criterion(Prop.Edc.ID, CatalogFilterExpressionOperator.LIKE, "%data-offer%"); + var query = criterion(providerConnector, Prop.Edc.ID, CatalogFilterExpressionOperator.LIKE, "%data-offer%"); // act var dataOffers = consumerClient.useCaseApi().queryCatalog(query); @@ -123,7 +89,12 @@ void catalog_filtering_by_like() { } - private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperator operator, String rightOperand) { + private CatalogQuery criterion( + ConnectorRemote providerConnector, + String leftOperand, + CatalogFilterExpressionOperator operator, + String rightOperand) { + return CatalogQuery.builder() .connectorEndpoint(getProtocolEndpoint(providerConnector)) .filterExpressions( @@ -138,7 +109,7 @@ private CatalogQuery criterion(String leftOperand, CatalogFilterExpressionOperat .build(); } - private void createAsset() { + private void createAsset(EdcClient providerClient) { var dataSource = UiDataSource.builder() .type(DataSourceType.HTTP_DATA) .httpData(UiDataSourceHttpData.builder() @@ -160,7 +131,7 @@ private void createAsset() { providerClient.uiApi().createAsset(asset); } - private void createPolicy() { + private void createPolicy(EdcClient providerClient) { var afterYesterday = UiPolicyConstraint.builder() .left("POLICY_EVALUATION_TIME") .operator(OperatorDto.GT) @@ -189,7 +160,7 @@ private void createPolicy() { providerClient.uiApi().createPolicyDefinition(policyDefinition); } - private void createContractDefinition() { + private void createContractDefinition(EdcClient providerClient) { var contractDefinition = ContractDefinitionRequest.builder() .contractDefinitionId(dataOfferId) .accessPolicyId(dataOfferId) diff --git a/utils/catalog-parser/build.gradle.kts b/utils/catalog-parser/build.gradle.kts index daf3707ce..dee5ebaf0 100644 --- a/utils/catalog-parser/build.gradle.kts +++ b/utils/catalog-parser/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { testCompileOnly(libs.lombok) testImplementation(project(":utils:test-utils")) testImplementation(libs.mockito.core) - testImplementation(libs.mockito.inline) testImplementation(libs.mockito.junitJupiter) testImplementation(libs.assertj.core) testImplementation(libs.junit.api) diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java index e9559cf2a..603bd2b27 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogService.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.catalog; diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java index 1d2a53dc0..7397afa15 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/DspCatalogServiceException.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.catalog; diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java deleted file mode 100644 index 53ad296ff..000000000 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.sovity.edc.utils.catalog.mapper; - -import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.jsonld.JsonLdUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import lombok.val; -import org.eclipse.edc.connector.contract.spi.ContractId; -import org.jetbrains.annotations.NotNull; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -public class DspContractOfferUtils { - - /** - * /!\ Workaround - *

      - * The Eclipse EDC uses a new random UUID for each policy that it returns and in turn a new contract ID. - * This Eclipse ID can't be used as such. - * As a workaround, we must introduce our own ID. - * For a first iteration, we will assume that the content of the policy remains the same (same content, same order) - * and hash it to use it as a key. - * - * @param contract The contract to compute an ID from - * @return A base64 string that can be used as an id for the {@code contract} - */ - public static String buildStableId(JsonObject contract) { - // NOTE: This doesn't enforce any property order and may cause trouble if the returned policy schema is not consistent. - // Use canonical form if needed later. - val noId = Json.createObjectBuilder(contract).remove(Prop.ID).build(); - val policyId = hash(noId); - - val currentId = ContractId.parseId(JsonLdUtils.string(contract, Prop.ID)) - .orElseThrow((failure) -> { - throw new RuntimeException("Failed to parse the contract id: " + failure.getFailureDetail()); - }); - - return currentId.definitionPart() + ":" + currentId.assetIdPart() + ":" + policyId; - } - - @NotNull - private static String hash(JsonObject noId) { - val policyJsonString = JsonUtils.toJson(noId); - val sha1 = sha1(policyJsonString); - // encoding with base16 to make the hash readable to humans (similarly to how the random UUID would have been readable) - val base16 = toBase16(sha1); - return toBase64(base16); - } - - @NotNull - private static String toBase64(String string) { - byte[] stringBytes = string.getBytes(StandardCharsets.UTF_8); - byte[] bytes = Base64.getEncoder().encode(stringBytes); - return new String(bytes); - } - - @NotNull - private static String toBase16(byte[] bytes) { - val sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(Character.forDigit(b >> 4 & 0xf, 16)); - sb.append(Character.forDigit(b & 0xf, 16)); - } - return sb.toString(); - } - - private static byte[] sha1(String string) { - try { - return MessageDigest.getInstance("sha-1").digest(string.getBytes()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } -} diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java index 02c0ec338..cf6444b84 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.catalog.mapper; diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java index e68bb07c5..0fdb5ef25 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspCatalog.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.catalog.model; diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java index a5150fc00..4ac17020e 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspContractOffer.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.catalog.model; diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java index 278098e35..e18f661c1 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/model/DspDataOffer.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.catalog.model; diff --git a/utils/json-and-jsonld-utils/build.gradle.kts b/utils/json-and-jsonld-utils/build.gradle.kts index d2ef943f4..18ed02190 100644 --- a/utils/json-and-jsonld-utils/build.gradle.kts +++ b/utils/json-and-jsonld-utils/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) testImplementation(libs.mockito.core) - testImplementation(libs.mockito.inline) testImplementation(libs.mockito.junitJupiter) testImplementation(libs.assertj.core) testImplementation(libs.junit.api) diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java index 1a91ae7cf..95c22d044 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/JsonUtils.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils; diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java index ec44ecdad..f264949af 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/JsonLdUtils.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.jsonld; diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index ea04f5e43..4a057ddd2 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.utils.jsonld.vocab; diff --git a/utils/test-connector-remote/build.gradle.kts b/utils/test-connector-remote/build.gradle.kts deleted file mode 100644 index a3d3d6474..000000000 --- a/utils/test-connector-remote/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ - -plugins { - `java-library` -} - -dependencies { - annotationProcessor(libs.lombok) - compileOnly(libs.lombok) - - api(libs.junit.api) - implementation(libs.apache.commonsLang) - - api(libs.edc.junit) - api(libs.awaitility.java) - api(project(":utils:json-and-jsonld-utils")) - implementation(project(":utils:versions")) - implementation(libs.edc.sqlCore) - implementation(libs.edc.jsonLdSpi) - implementation(libs.edc.jsonLd) - implementation(libs.assertj.core) - implementation(libs.testcontainers.testcontainers) - implementation(libs.testcontainers.junitJupiter) - implementation(libs.testcontainers.postgresql) - implementation(libs.restAssured.restAssured) -} - -group = libs.versions.sovityEdcExtensionGroup.get() - -publishing { - publications { - create(project.name) { - from(components["java"]) - } - } -} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java deleted file mode 100644 index af15b7822..000000000 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.sovity.edc.extension.e2e.db; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Delegate; -import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterResolver; - -import java.util.Map; -import java.util.function.Function; - -@RequiredArgsConstructor -public class EdcRuntimeExtensionWithTestDatabase - implements BeforeAllCallback, AfterAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { - - private final String moduleName; - private final String logPrefix; - - @Getter - @Delegate(types = {AfterAllCallback.class}) - private final TestDatabase testDatabase = new TestDatabaseViaTestcontainers(); - - private final Function> propertyFactory; - - @Delegate(types = { - BeforeTestExecutionCallback.class, - AfterTestExecutionCallback.class, - ParameterResolver.class - }) - @Getter - private EdcRuntimeExtensionFixed edcRuntimeExtension = null; - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - testDatabase.beforeAll(extensionContext); - edcRuntimeExtension = new EdcRuntimeExtensionFixed(moduleName, logPrefix, propertyFactory.apply(testDatabase)); - } -} diff --git a/utils/test-connector-remote/README.md b/utils/test-utils/README.md similarity index 75% rename from utils/test-connector-remote/README.md rename to utils/test-utils/README.md index 903aa7a09..fa298552d 100644 --- a/utils/test-connector-remote/README.md +++ b/utils/test-utils/README.md @@ -16,7 +16,11 @@ ## About this Utility -Connector Remote for creating simple test data via the management API. +A toolset to ease testing. + +* Connector Remote for creating simple test data via the management API. +* E2eTestExtension to bootstrap end-to-end tests using the wrapper API. +* EdcRuntimeExtensionWithTestDatabase to test single connectors with a database. ## Why does this extension exist? diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index 5aae2ca05..e2d6e9038 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -4,7 +4,29 @@ plugins { } dependencies { + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + api(libs.junit.api) + implementation(libs.apache.commonsLang) + + api(libs.edc.junit) + api(libs.awaitility.java) + api(project(":extensions:wrapper:clients:java-client")) + api(project(":utils:json-and-jsonld-utils")) + + implementation(project(":extensions:policy-always-true")) + implementation(project(":utils:versions")) + implementation(libs.edc.jsonLdSpi) + implementation(libs.edc.jsonLd) + implementation(libs.edc.sqlCore) + implementation(libs.assertj.core) + implementation(libs.jooq.jooq) + implementation(libs.mockserver.netty) + implementation(libs.testcontainers.testcontainers) + implementation(libs.testcontainers.junitJupiter) + implementation(libs.testcontainers.postgresql) + implementation(libs.restAssured.restAssured) } group = libs.versions.sovityEdcExtensionGroup.get() diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java similarity index 95% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java index e85911e77..ddc180bad 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/ConnectorRemote.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector; @@ -34,7 +35,6 @@ import java.net.URI; import java.time.Duration; -import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; @@ -56,8 +56,8 @@ @SuppressWarnings("java:S5960") @RequiredArgsConstructor public class ConnectorRemote { - @Getter + @Getter private final ConnectorRemoteConfig config; private final ObjectMapper objectMapper = JacksonJsonLd.createObjectMapper(); @@ -84,22 +84,6 @@ public void createAsset(String assetId, Map dataAddressPropertie .contentType(JSON); } - public List getAssetIds() { - var requestBody = createObjectBuilder() - .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add(TYPE, EDC_NAMESPACE + "QuerySpec") - .build(); - return prepareManagementApiCall() - .contentType(JSON) - .body(requestBody) - .when() - .post("/v2/assets/request") - .then() - .statusCode(200) - .contentType(JSON) - .extract().jsonPath().getList("@id"); - } - public String createPolicy(JsonObject policyJsonObject) { var requestBody = createObjectBuilder() .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java similarity index 98% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java index 88089a752..147debb37 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java similarity index 98% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java index e67af343b..00e68aeb0 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/MockDataAddressRemote.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java similarity index 97% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java index ff34e619b..383ce8c93 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfig.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java similarity index 75% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java index a4e3d76a4..4608b9396 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config; @@ -27,26 +28,12 @@ import static de.sovity.edc.extension.e2e.connector.config.DatasourceConfigUtils.configureDatasources; import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiConfigFactory.configureApi; import static org.eclipse.edc.junit.testfixtures.TestUtils.MAX_TCP_PORT; -import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ConnectorConfigFactory { private static final Random RANDOM = new Random(); - /** - * Creates the default configuration to start an EDC with the given test database. - * - * @deprecated Use {@link ConnectorConfigFactory#forTestDatabase(String, TestDatabase)} - * with automatic ports allocation to prevent port allocation conflicts. - */ - @Deprecated - public static ConnectorConfig forTestDatabase(String participantId, int firstPort, TestDatabase testDatabase) { - var config = basicEdcConfig(participantId, firstPort); - config.setProperties(configureDatasources(testDatabase.getJdbcCredentials())); - return config; - } - public static ConnectorConfig forTestDatabase(String participantId, TestDatabase testDatabase) { val firstPort = getFreePortRange(5); var config = basicEdcConfig(participantId, firstPort); @@ -56,16 +43,16 @@ public static ConnectorConfig forTestDatabase(String participantId, TestDatabase public static synchronized int getFreePortRange(int size) { // pick a random in a reasonable range - int firstPort = getFreePort(RANDOM.nextInt(10_000, 50_000)); + int firstPort = RANDOM.nextInt(10_000, 50_000); int currentPort = firstPort; do { - if (canUsePort(currentPort + 1)) { + if (canUsePort(currentPort)) { currentPort++; } else { - firstPort = getFreePort(currentPort++); + firstPort = currentPort++; } - } while (currentPort < firstPort + size); + } while (currentPort <= firstPort + size); return firstPort; } @@ -98,6 +85,8 @@ public static ConnectorConfig basicEdcConfig(String participantId, int firstPort properties.put("edc.last.commit.info", "test env commit message"); properties.put("edc.build.date", "2023-05-08T15:30:00Z"); + properties.put("edc.server.db.connection.timeout.in.ms", "5000"); + properties.put("my.edc.participant.id", participantId); properties.put("my.edc.title", "Connector Title %s".formatted(participantId)); properties.put("my.edc.description", "Connector Description %s".formatted(participantId)); @@ -106,6 +95,17 @@ public static ConnectorConfig basicEdcConfig(String participantId, int firstPort properties.put("my.edc.maintainer.url", "http://maintainer.%s".formatted(participantId)); properties.put("my.edc.maintainer.name", "Maintainer Name %s".formatted(participantId)); + properties.put("edc.server.db.connection.pool.size", "3"); + + properties.put("web.http.port", String.valueOf(apiConfig.getDefaultApiGroup().port())); + properties.put("web.http.path", String.valueOf(apiConfig.getDefaultApiGroup().path())); + properties.put("web.http.protocol.port", String.valueOf(apiConfig.getProtocolApiGroup().port())); + properties.put("web.http.protocol.path", String.valueOf(apiConfig.getProtocolApiGroup().path())); + properties.put("web.http.management.port", String.valueOf(apiConfig.getManagementApiGroup().port())); + properties.put("web.http.management.path", String.valueOf(apiConfig.getManagementApiGroup().path())); + properties.put("web.http.control.port", String.valueOf(apiConfig.getControlApiGroup().port())); + properties.put("web.http.control.path", String.valueOf(apiConfig.getControlApiGroup().path())); + return new ConnectorConfig( participantId, apiConfig.getDefaultApiGroup(), diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java similarity index 96% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java index 69dbcf302..8577b1d92 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfig.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java similarity index 99% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java index 54f08c8fc..e3d9c4440 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorRemoteConfigFactory.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java similarity index 98% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java index fd1df4057..47b822453 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/DatasourceConfigUtils.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java similarity index 97% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java index d1cc4dd36..4aca4ef89 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfig.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java similarity index 99% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java index 603172ea4..5f5f26302 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiConfigFactory.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java similarity index 97% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java index 3af481919..46bc7dcb6 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroup.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java similarity index 96% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java index 78c610da5..af579097e 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/EdcApiGroupConfig.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java similarity index 95% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java index f747ed6e2..1b5919dfe 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/ApiKeyAuthProvider.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api.auth; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java similarity index 93% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java index 35474d8a9..a2e11a921 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/AuthProvider.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api.auth; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java similarity index 95% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java index 3181c9453..af59e7eb3 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/api/auth/NoneAuthProvider.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.connector.config.api.auth; diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionDeferred.java diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java new file mode 100644 index 000000000..868c3d89d --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java @@ -0,0 +1,69 @@ +package de.sovity.edc.extension.e2e.db; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import lombok.val; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import java.util.Map; +import java.util.function.Function; + +@RequiredArgsConstructor +public class EdcRuntimeExtensionWithTestDatabase + implements BeforeAllCallback, AfterAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { + + private final String moduleName; + private final String logPrefix; + + @Getter + @Delegate(types = {AfterAllCallback.class}) + private final TestDatabase testDatabase = new TestDatabaseViaTestcontainers(); + + private final Function> propertyFactory; + + @Delegate(types = { + BeforeTestExecutionCallback.class, + AfterTestExecutionCallback.class + }) + @Getter + private EdcRuntimeExtensionFixed edcRuntimeExtension = null; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + testDatabase.beforeAll(extensionContext); + edcRuntimeExtension = new EdcRuntimeExtensionFixed(moduleName, logPrefix, propertyFactory.apply(testDatabase)); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + boolean isJooqDsl = parameterContext.getParameter().getType().equals(DSLContext.class); + return isJooqDsl || edcRuntimeExtension.supportsParameter(parameterContext, extensionContext); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + boolean isJooqDsl = parameterContext.getParameter().getType().equals(DSLContext.class); + if (isJooqDsl) { + return getDslContext().dsl(); + } else { + return edcRuntimeExtension.resolveParameter(parameterContext, extensionContext); + } + } + + private synchronized DSLContext getDslContext() { + val credentials = testDatabase.getJdbcCredentials(); + return DSL.using(credentials.jdbcUrl(), credentials.jdbcUser(), credentials.jdbcPassword()); + } +} diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/JdbcCredentials.java diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabase.java diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseFactory.java diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaEnvVars.java diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java similarity index 100% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/TestDatabaseViaTestcontainers.java diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java similarity index 97% rename from utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java rename to utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java index 1b1c1bd69..9a2cc713a 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/env/EnvUtil.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * sovity GmbH - init + * */ package de.sovity.edc.extension.e2e.env; diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Consumer.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Consumer.java new file mode 100644 index 000000000..cbd622765 --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Consumer.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * In test code, in conjunction with {@link E2eTestExtension}, specifies that the injected instance must come from the Consumer EDC + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Consumer { +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java new file mode 100644 index 000000000..a4bc6317d --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.ContractDefinitionRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationRequest; +import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.ContractTerminationRequest; +import de.sovity.edc.client.gen.model.DataSourceType; +import de.sovity.edc.client.gen.model.IdResponseDto; +import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiContractNegotiation; +import de.sovity.edc.client.gen.model.UiCriterion; +import de.sovity.edc.client.gen.model.UiCriterionLiteral; +import de.sovity.edc.client.gen.model.UiCriterionLiteralType; +import de.sovity.edc.client.gen.model.UiCriterionOperator; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceHttpData; +import de.sovity.edc.client.gen.model.UiPolicyConstraint; +import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyLiteral; +import de.sovity.edc.client.gen.model.UiPolicyLiteralType; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import lombok.val; +import org.awaitility.Awaitility; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.RUNNING; +import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; +import static java.time.Duration.ofSeconds; +import static org.assertj.core.api.Assertions.assertThat; + +public class E2eScenario { + private final ConnectorConfig consumerConfig; + private final ConnectorConfig providerConfig; + private final ClientAndServer mockServer; + private final Duration timeout = ofSeconds(10); + + private EdcClient consumerClient; + private EdcClient providerClient; + + public E2eScenario(ConnectorConfig consumerConfig, ConnectorConfig providerConfig, ClientAndServer mockServer) { + this.consumerConfig = consumerConfig; + this.providerConfig = providerConfig; + this.mockServer = mockServer; + + consumerClient = EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + + providerClient = EdcClient.builder() + .managementApiUrl(providerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(providerConfig.getProperties().get("edc.api.auth.key")) + .build(); + } + + private final String alwaysTruePolicyId = POLICY_DEFINITION_ID; + + private final AtomicInteger assetCounter = new AtomicInteger(0); + + public String createAsset() { + val dummyDataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com") + .build()) + .build(); + + return internalCreateAsset("asset-" + assetCounter.getAndIncrement(), dummyDataSource).getId(); + } + + public String createAsset(String id, UiDataSourceHttpData uiDataSourceHttpData) { + val uiDataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(uiDataSourceHttpData) + .build(); + + return internalCreateAsset(id, uiDataSource).getId(); + } + + public MockedAsset createAssetWithMockResource(String id) { + + val path = "/assets/" + id; + val url = "http://localhost:" + mockServer.getPort() + path; + + val uiDataSource = UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder().baseUrl(url).build()) + .build(); + + val accesses = new AtomicInteger(0); + + mockServer.when(HttpRequest.request(path).withMethod("GET")).respond(it -> { + accesses.incrementAndGet(); + return HttpResponse.response().withStatusCode(200); + }); + + internalCreateAsset(id, uiDataSource); + + return new MockedAsset(id, accesses); + } + + private IdResponseDto internalCreateAsset(String assetId, UiDataSource dataSource) { + return providerClient.uiApi() + .createAsset(UiAssetCreateRequest.builder() + .id(assetId) + .title("AssetName " + assetId) + .version("1.0.0") + .language("en") + .dataSource(dataSource) + .build()); + } + + public String createPolicyDefinition(String policyId, UiPolicyConstraint... constraints) { + return createPolicyDefinition(policyId, Arrays.stream(constraints).toList()).getId(); + } + + private IdResponseDto createPolicyDefinition(String policyId, List constraints) { + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(policyId) + .policy(UiPolicyCreateRequest.builder() + .constraints(constraints) + .build() + ) + .build(); + + return providerClient.uiApi().createPolicyDefinition(policyDefinition); + } + + public String createContractDefinition(String assetId) { + return createContractDefinition(POLICY_DEFINITION_ID, assetId).getId(); + } + + public IdResponseDto createContractDefinition(String policyId, String assetId) { + return providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId("cd-" + policyId + "-" + assetId) + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId) + .build()) + .build())) + .build()); + } + + public UiContractNegotiation negotiateAssetAndAwait(String assetId) { + val connectorEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); + val offers = consumerClient.uiApi().getCatalogPageDataOffers(connectorEndpoint); + + val offersContainingContract = offers.stream() + .filter(offer -> offer.getAsset().getAssetId().equals(assetId)) + .toList(); + + assertThat(offersContainingContract).hasSize(1); + + val firstContractOffer = offersContainingContract.get(0).getContractOffers().get(0); + val dataOffer = offersContainingContract.get(0); + var negotiationRequest = ContractNegotiationRequest.builder() + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(firstContractOffer.getContractOfferId()) + .policyJsonLd(firstContractOffer.getPolicy().getPolicyJsonLd()) + .build(); + + val negotiation = consumerClient.uiApi().initiateContractNegotiation(negotiationRequest); + + val neg = Awaitility.await().atMost(timeout).until( + () -> consumerClient.uiApi().getContractNegotiation(negotiation.getContractNegotiationId()), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + ); + + assertThat(neg.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); + + return neg; + } + + public void createPolicy(String id, OffsetDateTime from, OffsetDateTime until) { + val startConstraint = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(from.toString()) + .build()) + .build(); + + val endConstraint = UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.LT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(until.toString()) + .build()) + .build(); + + var policyDefinition = PolicyDefinitionCreateRequest.builder() + .policyDefinitionId(id) + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(startConstraint, endConstraint)) + .build()) + .build(); + + providerClient.uiApi().createPolicyDefinition(policyDefinition); + } + + public String transferAndAwait(InitiateTransferRequest transferRequest) { + val transferInit = consumerClient.uiApi().initiateTransfer(transferRequest).getId(); + awaitTransferCompletion(transferInit); + return transferInit; + } + + public String transferAndAwait(InitiateCustomTransferRequest transferRequest) { + val transferInit = consumerClient.uiApi().initiateCustomTransfer(transferRequest).getId(); + awaitTransferCompletion(transferInit); + return transferInit; + } + + public void awaitTransferCompletion(String transferId) { + Awaitility.await().atMost(timeout).until( + () -> consumerClient.uiApi() + .getTransferHistoryPage() + .getTransferEntries() + .stream() + .filter(it -> it.getTransferProcessId().equals(transferId)) + .findFirst() + .map(it -> it.getState().getSimplifiedState()), + it -> it.orElse(RUNNING) != RUNNING + ); + } + + public IdResponseDto terminateContractAgreementAndAwait( + ContractNegotiation.Type party, + String contractAgreementId, + ContractTerminationRequest terminationRequest + ) { + if (party.equals(ContractNegotiation.Type.CONSUMER)) { + return consumerClient.uiApi().terminateContractAgreement(contractAgreementId, terminationRequest); + } else { + return providerClient.uiApi().terminateContractAgreement(contractAgreementId, terminationRequest); + } + } +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java new file mode 100644 index 000000000..ddbd592ae --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.extension.e2e.connector.ConnectorRemote; +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfig; +import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; +import de.sovity.edc.extension.utils.Lazy; +import lombok.val; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.mockserver.integration.ClientAndServer; + +import java.util.List; +import java.util.stream.Stream; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; +import static org.mockserver.stop.Stop.stopQuietly; + +public class E2eTestExtension + implements BeforeAllCallback, AfterAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { + + private final String consumerParticipantId; + private ConnectorConfig consumerConfig; + private final EdcRuntimeExtensionWithTestDatabase consumerExtension; + + private final String providerParticipantId; + private ConnectorConfig providerConfig; + private final EdcRuntimeExtensionWithTestDatabase providerExtension; + + private final List> partySupportedTypes = List.of(ConnectorConfig.class, EdcClient.class, ConnectorRemote.class, ClientAndServer.class); + private final List> supportedTypes = Stream.concat(partySupportedTypes.stream(), Stream.of(E2eScenario.class)).toList(); + + private Lazy clientAndServer; + + public E2eTestExtension() { + this("consumer", "provider"); + } + + public E2eTestExtension(String consumerParticipantId, String providerParticipantId) { + this.consumerParticipantId = consumerParticipantId; + this.providerParticipantId = providerParticipantId; + + consumerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "consumer", + testDatabase -> { + consumerConfig = forTestDatabase(this.consumerParticipantId, testDatabase); + return consumerConfig.getProperties(); + } + ); + providerExtension = new EdcRuntimeExtensionWithTestDatabase( + ":launchers:connectors:sovity-dev", + "provider", + testDatabase -> { + providerConfig = forTestDatabase(this.providerParticipantId, testDatabase); + return providerConfig.getProperties(); + } + ); + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + consumerExtension.beforeAll(context); + providerExtension.beforeAll(context); + } + + @Override + public void beforeTestExecution(ExtensionContext context) throws Exception { + clientAndServer = new Lazy<>(() -> ClientAndServer.startClientAndServer(getFreePort())); + consumerExtension.beforeTestExecution(context); + providerExtension.beforeTestExecution(context); + } + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + if (clientAndServer.isInitialized()) { + stopQuietly(clientAndServer.get()); + } + consumerExtension.afterTestExecution(context); + providerExtension.afterTestExecution(context); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + consumerExtension.afterAll(context); + providerExtension.afterAll(context); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + val isProvider = isProvider(parameterContext); + val isConsumer = isConsumer(parameterContext); + + if (isProvider && isConsumer) { + throw new ParameterResolutionException("Either @Provider or @Consumer may be used."); + } + + val type = parameterContext.getParameter().getType(); + + if (isConsumer) { + return partySupportedTypes.contains(type) || consumerExtension.supportsParameter(parameterContext, extensionContext); + } + + if (isProvider) { + return partySupportedTypes.contains(type) || providerExtension.supportsParameter(parameterContext, extensionContext); + } + + if (supportedTypes.contains(type)) { + return true; + } + + return consumerExtension.supportsParameter(parameterContext, extensionContext) || + providerExtension.supportsParameter(parameterContext, extensionContext); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + val isConsumer = isConsumer(parameterContext); + val isProvider = isProvider(parameterContext); + + val type = parameterContext.getParameter().getType(); + + if (isConsumer) { + if (EdcClient.class.equals(type)) { + return newEdcClient(consumerConfig); + } else if (ConnectorConfig.class.equals(type)) { + return consumerConfig; + } else if (ConnectorRemote.class.equals(type)) { + return newConnectorRemote(consumerParticipantId, consumerConfig); + } else { + return consumerExtension.resolveParameter(parameterContext, extensionContext); + } + } + + if (isProvider) { + if (EdcClient.class.equals(type)) { + return newEdcClient(providerConfig); + } else if (ConnectorConfig.class.equals(type)) { + return providerConfig; + } else if (ConnectorRemote.class.equals(type)) { + return newConnectorRemote(providerParticipantId, providerConfig); + } else { + return providerExtension.resolveParameter(parameterContext, extensionContext); + } + } + + if (E2eScenario.class.equals(type)) { + return new E2eScenario(consumerConfig, providerConfig, clientAndServer.get()); + } else if (ClientAndServer.class.equals(type)) { + return clientAndServer.get(); + } + + throw new IllegalArgumentException( + "The parameters must be annotated by the EDC side: @Provider or @Consumer or be one of the supported classes."); + } + + private @NotNull ConnectorRemote newConnectorRemote(String participantId, ConnectorConfig config) { + return new ConnectorRemote( + new ConnectorRemoteConfig( + participantId, + config.getDefaultEndpoint(), + config.getManagementEndpoint(), + config.getProtocolEndpoint())); + } + + private static boolean isProvider(ParameterContext parameterContext) { + return parameterContext.getParameter().getDeclaredAnnotation(Provider.class) != null; + } + + private static boolean isConsumer(ParameterContext parameterContext) { + return parameterContext.getParameter().getDeclaredAnnotation(Consumer.class) != null; + } + + private EdcClient newEdcClient(ConnectorConfig consumerConfig) { + return EdcClient.builder() + .managementApiUrl(consumerConfig.getManagementEndpoint().getUri().toString()) + .managementApiKey(consumerConfig.getProperties().get("edc.api.auth.key")) + .build(); + } +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/MockedAsset.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/MockedAsset.java new file mode 100644 index 000000000..45609e48f --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/MockedAsset.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An asset that uses a mocked network resource. + * + * @param assetId The related asset's ID + * @param networkAccesses How many times the resource was accessed via the network. + */ +public record MockedAsset( + String assetId, + AtomicInteger networkAccesses +) { +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Provider.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Provider.java new file mode 100644 index 000000000..e3cff34b2 --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Provider.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * In test code, in conjunction with {@link E2eTestExtension}, specifies that the injected instance must come from the Provider EDC + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Provider { +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/Lazy.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/Lazy.java new file mode 100644 index 000000000..ce89df5ee --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/Lazy.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.utils; + +import lombok.RequiredArgsConstructor; + +import java.util.function.Supplier; + +@RequiredArgsConstructor +public class Lazy { + private final Supplier supplier; + + private T tt; + + public synchronized T get() { + if (tt == null) { + tt = supplier.get(); + } + + return tt; + } + + public boolean isInitialized() { + return tt != null; + } +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java index 6c91ac61c..f0382d096 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/utils/junit/DisabledOnGithub.java @@ -11,6 +11,7 @@ * sovity GmbH - initial API and implementation * */ + package de.sovity.edc.extension.utils.junit; import org.junit.jupiter.api.Tag; From b2219a8dcdbd43cb4c1cc69739d5b0f540883d78 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 18 Jul 2024 14:59:31 +0200 Subject: [PATCH 264/295] fix: Pool size must be optional (#1000) --- CHANGELOG.md | 12 +++++++++--- .../directaccess/DatabaseDirectAccessExtension.java | 10 ++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 244b9dc34..584c1e5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Deployment Migration Notes +New configuration to access the database: + +- `EDC_SERVER_DB_CONNECTION_POOL_SIZE` + - The property controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. Basically this value will determine the maximum number of actual connections to the database backend. + - Defaults to `3` + #### Compatible Versions - Connector Backend Docker Images: @@ -41,7 +47,7 @@ MDS 2.2 intermediate release - API Wrapper UI API: Data sources are now well-typed. - The Broker has been removed in favor of the Authority Portal: - - A new Deployment Unit, the ["Data Catalog Crawler"](extensions/catalog-crawler/README.md), has been added. + - A new Deployment Unit, the ["Data Catalog Crawler"](extensions/catalog-crawler/README.md), has been added. - Each "Data Catalog Crawler" connects to an existing Authority Portal Deployment's DB. - Each "Data Catalog Crawler" is responsible for crawling exactly one environment. - The Data Catalog functionality of the Broker has been integrated into the Authority Portal. @@ -63,8 +69,8 @@ MDS 2.2 intermediate release - Connector: - The database migration system has been moved from multiple migration history tables to a single one. - Broker: - - The broker has been removed. For Authority Portal users, please check out the new - [Data Catalog Crawler Productive Deployment Guide](docs/deployment-guide/goals/catalog-crawler-production/README.md). + - The broker has been removed. For Authority Portal users, please check out the new + [Data Catalog Crawler Productive Deployment Guide](docs/deployment-guide/goals/catalog-crawler-production/README.md). - Any previous broker deployment's database is not required anymore. - Please care that only some environment variables look similar. It is recommended to create fresh deployments. diff --git a/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java index a69d4eba8..480fb0d41 100644 --- a/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java +++ b/extensions/database-direct-access/src/main/java/de/sovity/edc/extension/db/directaccess/DatabaseDirectAccessExtension.java @@ -50,12 +50,6 @@ public class DatabaseDirectAccessExtension implements ServiceExtension { @Setting(defaultValue = "3") public static final String EDC_SERVER_DB_CONNECTION_POOL_SIZE = "edc.server.db.connection.pool.size"; - /** - * Sets the connection timeout for the datasource in milliseconds. - */ - @Setting(defaultValue = "5000") - public static final String EDC_SERVER_DB_CONNECTION_TIMEOUT_IN_MS = "edc.server.db.connection.timeout.in.ms"; - @Override public String name() { @@ -75,11 +69,11 @@ private void initializeDirectDatabaseAccess(ServiceExtensionContext context) { hikariConfig.setUsername(config.getString(EDC_DATASOURCE_JDBC_USER)); hikariConfig.setPassword(config.getString(EDC_DATASOURCE_JDBC_PASSWORD)); hikariConfig.setMinimumIdle(1); - hikariConfig.setMaximumPoolSize(config.getInteger(EDC_SERVER_DB_CONNECTION_POOL_SIZE)); + hikariConfig.setMaximumPoolSize(config.getInteger(EDC_SERVER_DB_CONNECTION_POOL_SIZE, 3)); hikariConfig.setIdleTimeout(30000); hikariConfig.setPoolName("direct-database-access"); hikariConfig.setMaxLifetime(50000); - hikariConfig.setConnectionTimeout(config.getInteger(EDC_SERVER_DB_CONNECTION_TIMEOUT_IN_MS)); + hikariConfig.setConnectionTimeout(1000); val dda = new DslContextFactory(new HikariDataSource(hikariConfig)); From 60dbc7c32f00f96b10e20286739c04c7c3594dfb Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 18 Jul 2024 18:32:07 +0200 Subject: [PATCH 265/295] feat: Update postman and add details check (#1001) --- docker-compose-dev.yaml | 2 + docs/api/postman_collection.json | 54 ++++++++++++++++--- docs/api/sovity-edc-api-wrapper.yaml | 1 + docs/dev/debugging.md | 50 +++++++++++++++++ .../edc/ext/wrapper/api/ui/UiResource.java | 2 - .../ui/model/ContractTerminationRequest.java | 11 +++- .../edc/e2e/ContractTerminationTest.java | 43 ++++++++++++++- 7 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 docs/dev/debugging.md diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 01d93ec40..8dfd7e1af 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -42,6 +42,7 @@ services: EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' EDC_WEB_REST_CORS_ORIGINS: '*' EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: - '11001:11001' - '11002:11002' @@ -89,6 +90,7 @@ services: EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' EDC_WEB_REST_CORS_ORIGINS: '*' EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: - '22001:11001' - '22002:11002' diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 7c9901f10..ef1d557fa 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "c01dc51f-eb36-43db-b8db-a33ff2f5baa5", + "_postman_id": "a842e697-4b5e-4308-b802-35c446ea6135", "name": "sovity EDC Community Edition", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31514741" + "_exporter_id": "32949497" }, "item": [ { @@ -84,7 +84,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"id\": \"testname-v1.0\",\r\n \"title\": \"TestName\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.google.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n }\r\n}", + "raw": "{\r\n \"id\": \"testname-v1.0\",\r\n \"title\": \"TestName\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.google.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n },\r\n \"dataSource\": {\r\n \"type\": \"HTTP_DATA\",\r\n \"httpData\": {\r\n \"baseUrl\": \"http://example.com/baseUrl/\"\r\n }\r\n }\r\n}", "options": { "raw": { "language": "json" @@ -386,7 +386,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"contractDefinitionId\": \"testCD\",\r\n \"contractPolicyId\": \"always-true\",\r\n \"accessPolicyId\": \"always-true\",\r\n \"assetSelector\": [\r\n {\r\n \"operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\r\n \"operator\": \"IN\",\r\n \"operandRight\": {\r\n \"type\": \"VALUE_LIST\",\r\n \"valueList\": [\r\n \"testparamterization-v1.0\"\r\n ]\r\n }\r\n }\r\n ]\r\n}", + "raw": "{\r\n \"contractDefinitionId\": \"testCD\",\r\n \"contractPolicyId\": \"always-true\",\r\n \"accessPolicyId\": \"always-true\",\r\n \"assetSelector\": [\r\n {\r\n \"operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\r\n \"operator\": \"IN\",\r\n \"operandRight\": {\r\n \"type\": \"VALUE_LIST\",\r\n \"valueList\": [\r\n \"testname-v1.0\"\r\n ]\r\n }\r\n }\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -476,7 +476,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"counterPartyAddress\": \"http://edc:11003/api/dsp\",\r\n \"counterPartyParticipantId\": \"my-edc\",\r\n \"contractOfferId\": \"dGVzdENE:dGVzdHBhcmFtdGVyaXphdGlvbi12MS4w:NTUxNjZiOGMtMzk4My00MWMzLTkxN2UtZTQ1ZGVlNzMyNTlj\",\r\n \"policyJsonLd\": \"{\\\"@id\\\":\\\"f15e4b97-0d99-42f5-8f6a-525daf0b72c6\\\",\\\"@type\\\":\\\"http://www.w3.org/ns/odrl/2/Set\\\",\\\"http://www.w3.org/ns/odrl/2/permission\\\":[{\\\"http://www.w3.org/ns/odrl/2/target\\\":\\\"testparamterization-v1.0\\\",\\\"http://www.w3.org/ns/odrl/2/action\\\":{\\\"http://www.w3.org/ns/odrl/2/type\\\":\\\"USE\\\"},\\\"http://www.w3.org/ns/odrl/2/constraint\\\":[{\\\"http://www.w3.org/ns/odrl/2/leftOperand\\\":{\\\"@value\\\":\\\"ALWAYS_TRUE\\\"},\\\"http://www.w3.org/ns/odrl/2/operator\\\":[{\\\"@id\\\":\\\"http://www.w3.org/ns/odrl/2/eq\\\"}],\\\"http://www.w3.org/ns/odrl/2/rightOperand\\\":{\\\"@value\\\":\\\"true\\\"}}]}],\\\"http://www.w3.org/ns/odrl/2/prohibition\\\":[],\\\"http://www.w3.org/ns/odrl/2/obligation\\\":[],\\\"http://www.w3.org/ns/odrl/2/target\\\":\\\"testparamterization-v1.0\\\"}\",\r\n \"assetId\": \"testparamterization-v1.0\"\r\n}", + "raw": "{\r\n \"counterPartyAddress\": \"http://edc:11003/api/dsp\",\r\n \"counterPartyParticipantId\": \"my-edc\",\r\n \"contractOfferId\": \"{{CONTRACT_OFFER_ID}}\",\r\n \"policyJsonLd\": \"{{POLICY_JSON_LD}}\",\r\n \"assetId\": \"{{ASSET_ID}}\"\r\n}", "options": { "raw": { "language": "json" @@ -545,6 +545,48 @@ } }, "response": [] + }, + { + "name": "Terminate Contract Agreement", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"detail\": \"Some detail between 1 and 1000 chars long\",\n \"reason\": \"Some reason between 1 and 100 chars\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/content-agreement-page/{{CONTRACT_AGREEMENT_ID}}/terminate", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "content-agreement-page", + "{{CONTRACT_AGREEMENT_ID}}", + "terminate" + ] + } + }, + "response": [] } ] }, @@ -1960,4 +2002,4 @@ "type": "default" } ] -} +} \ No newline at end of file diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 851967da8..eb0ab902c 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -1786,6 +1786,7 @@ components: description: "For type PARAMS_ONLY: Required data for starting a Transfer Process" ContractTerminationRequest: required: + - detail - reason type: object properties: diff --git a/docs/dev/debugging.md b/docs/dev/debugging.md new file mode 100644 index 000000000..0bf1f90b8 --- /dev/null +++ b/docs/dev/debugging.md @@ -0,0 +1,50 @@ +# Debugging + +## Connect to docker images + +### Configure + +The images are started with the [docker-entrypoint.sh](../../launchers/docker-entrypoint.sh). + +This entry point supports debugging via environment variables. + +In `docker-compose-dev`, add the following environment variables in `services.edc.environment` + +```yaml + REMOTE_DEBUG: y + REMOTE_DEBUG_BIND: 0.0.0.0:5005 +``` + +If you also want to wait for the debugger to connect before starting the EDC, also add + +```yaml + REMOTE_DEBUG_SUSPEND: y +``` + +Then shutdown and restart the EDC with docker compose. + +If you used the `dev` set of files: + +```bash +docker compose --env-file .env.dev --file docker-compose-dev.yaml down +docker compose --env-file .env.dev --file docker-compose-dev.yaml up +``` + +### Connect + +Each EDC will start on a different set of ports, but they all bind to the same address mentioned above in docker. + +To connect to the EDC, in IJ, do: + +* Edit configurations +* Add New Configuration +* Remote JVM debugger +* Debugger mode: attach to remote JVM +* Host: localhost +* Post: it depends on the EDC: + * check the `ports` mapping in the docker compose file where you added the remote debugging options + * look for the entry that is `#####:5005` + * This `#####` port is the port you want to connect to. +* Use module classpath: use sovity-edc-ce + +Enjoy! :) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 9c28a1be2..5b8e8eece 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -34,10 +34,8 @@ import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java index 6f570e607..b0615f2ae 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractTerminationRequest.java @@ -15,6 +15,9 @@ package de.sovity.edc.ext.wrapper.api.ui.model; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; @@ -35,8 +38,11 @@ public class ContractTerminationRequest { @Schema( title = "Termination detail", description = "A user explanation to detail why the contract was terminated.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.REQUIRED) @Size(max = MAX_DETAIL_SIZE) + @NotNull + @NotEmpty + @NotBlank String detail; @Schema( @@ -44,5 +50,8 @@ public class ContractTerminationRequest { description = "A short reason why this contract was terminated", requiredMode = Schema.RequiredMode.REQUIRED) @Size(max = MAX_REASON_SIZE) + @NotBlank + @NotEmpty + @NotNull String reason; } diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index a73735d83..595619c5d 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -33,13 +33,16 @@ import org.awaitility.Awaitility; import org.eclipse.edc.connector.contract.spi.ContractId; import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.ExtendWith; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import java.time.OffsetDateTime; +import java.util.List; import java.util.Map; import java.util.stream.IntStream; @@ -52,11 +55,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; @ExtendWith(E2eTestExtension.class) public class ContractTerminationTest { - @Test void canGetAgreementPageForNonTerminatedContract( E2eScenario scenario, @@ -249,6 +252,44 @@ void limitTheDetailSizeAt1000Chars( // termination completed == success } + @TestFactory + List theDetailsAreMandatory( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Provider EdcClient providerClient + ) { + val invalidDetails = List.of( + "", + " ", + " ", + "\t", + "\n" + ); + + return invalidDetails.stream().map( + detail -> dynamicTest("Can't use '%s' for details".formatted(detail), + () -> { + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + // act + val reason = "Some reason"; + + // assert when too big + + assertThrows( + ApiException.class, + () -> consumerClient.uiApi().terminateContractAgreement( + negotiation.getContractAgreementId(), + ContractTerminationRequest.builder() + .detail(detail) + .reason(reason) + .build()) + ); + })).toList(); + } + @Test @SneakyThrows void canTerminateFromProvider( From aae947b4eb7504c35067b01545c6d36b9dd186e8 Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:48:23 +0200 Subject: [PATCH 266/295] feat: id availability check endpoints (#1003) --------- Co-authored-by: Christophe Loiseau --- CHANGELOG.md | 1 + docs/api/postman_collection.json | 75 ++- docs/api/sovity-edc-api-wrapper.yaml | 69 +++ ...tAgreementTerminationDetailsQueryTest.java | 2 + .../edc/ext/wrapper/api/ui/UiResource.java | 19 + .../api/ui/model/IdAvailabilityResponse.java | 21 + .../WrapperExtensionContextBuilder.java | 5 +- .../ext/wrapper/api/ui/UiResourceImpl.java | 25 +- .../data_offer/DataOfferPageApiService.java | 45 ++ .../edc/e2e/ContractTerminationTest.java | 1 + .../de/sovity/edc/e2e/UiApiWrapperTest.java | 483 ++++++++++-------- 11 files changed, 512 insertions(+), 234 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdAvailabilityResponse.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 584c1e5bd..9eac36b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes - Both providers and consumers can now terminate their contracts. +- Added endpoints for checking ID availability for policies, assets and contract definitions #### Patch Changes diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index ef1d557fa..21b2363a0 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "a842e697-4b5e-4308-b802-35c446ea6135", + "_postman_id": "14267003-be4b-40ee-a9b6-83b5dd32ba57", "name": "sovity EDC Community Edition", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -735,6 +735,77 @@ ] } ] + }, + { + "name": "Data Offer", + "item": [ + { + "name": "Check Policy ID Availability", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/data-offer-page/validate-policy-id/{{POLICY_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "data-offer-page", + "validate-policy-id", + "{{POLICY_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Check Asset ID Availability", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/data-offer-page/validate-asset-id/{{ASSET_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "data-offer-page", + "validate-asset-id", + "{{ASSET_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Check Contract Definition ID Availability", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/data-offer-page/validate-contract-definition-id/{{CONTRACT_DEFINITION_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "data-offer-page", + "validate-contract-definition-id", + "{{CONTRACT_DEFINITION_ID}}" + ] + } + }, + "response": [] + } + ] } ] }, @@ -2002,4 +2073,4 @@ "type": "default" } ] -} \ No newline at end of file +} diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index eb0ab902c..c466cbb97 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -413,6 +413,63 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/data-offer-page/validate-asset-id/{assetId}: + get: + tags: + - UI + description: Validates if the provided assetId is already taken + operationId: isAssetIdAvailable + parameters: + - name: assetId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdAvailabilityResponse' + /wrapper/ui/pages/data-offer-page/validate-contract-definition-id/{contractDefinitionId}: + get: + tags: + - UI + description: Validates if the provided contractDefinitionId is already taken + operationId: isContractDefinitionIdAvailable + parameters: + - name: contractDefinitionId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdAvailabilityResponse' + /wrapper/ui/pages/data-offer-page/validate-policy-id/{policyId}: + get: + tags: + - UI + description: Validates if the provided policyId is already taken + operationId: isPolicyIdAvailable + parameters: + - name: policyId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdAvailabilityResponse' /wrapper/ui/pages/content-agreement-page/{contractAgreementId}/terminate: post: tags: @@ -1784,6 +1841,18 @@ components: description: Additional transfer process properties. These are not passed to the consumer EDC description: "For type PARAMS_ONLY: Required data for starting a Transfer Process" + IdAvailabilityResponse: + required: + - id + type: object + properties: + id: + type: string + description: ID + available: + type: boolean + description: Object indicates whether an ID for the given object type is already + taken or not ContractTerminationRequest: required: - detail diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java index a13d156c9..a3baebb4b 100644 --- a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java @@ -23,6 +23,7 @@ import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import lombok.val; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,6 +35,7 @@ @ExtendWith(E2eTestExtension.class) class ContractAgreementTerminationDetailsQueryTest { + @DisabledOnGithub @Test void fetchAgreementDetailsOrThrow_whenAgreementIsPresent_shouldReturnTheAgreementDetails( E2eScenario scenario, diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 5b8e8eece..dd09224c1 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; @@ -187,4 +188,22 @@ IdResponseDto terminateContractAgreement( @Produces(MediaType.APPLICATION_JSON) @Operation(description = "Queries a transfer process' asset") UiAsset getTransferProcessAsset(@PathParam("transferProcessId") String transferProcessId); + + @GET + @Path("pages/data-offer-page/validate-policy-id/{policyId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Validates if the provided policyId is already taken") + IdAvailabilityResponse isPolicyIdAvailable(@PathParam("policyId") String policyId); + + @GET + @Path("pages/data-offer-page/validate-asset-id/{assetId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Validates if the provided assetId is already taken") + IdAvailabilityResponse isAssetIdAvailable(@PathParam("assetId") String assetId); + + @GET + @Path("pages/data-offer-page/validate-contract-definition-id/{contractDefinitionId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Validates if the provided contractDefinitionId is already taken") + IdAvailabilityResponse isContractDefinitionIdAvailable(@PathParam("contractDefinitionId") String contractDefinitionId); } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdAvailabilityResponse.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdAvailabilityResponse.java new file mode 100644 index 000000000..8d9956d1c --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/IdAvailabilityResponse.java @@ -0,0 +1,21 @@ +package de.sovity.edc.ext.wrapper.api.ui.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Object indicates whether an ID for the given object type is already taken or not") +public class IdAvailabilityResponse { + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) + private final String id; + + @Schema(description = "ID Availability", requiredMode = Schema.RequiredMode.REQUIRED) + private boolean isAvailable; +} + diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 5a1d41324..9613f64e1 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -61,6 +61,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.MiwConfigService; import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.OwnConnectorEndpointServiceImpl; import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; +import de.sovity.edc.ext.wrapper.api.ui.pages.data_offer.DataOfferPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; @@ -252,6 +253,7 @@ public static WrapperExtensionContext buildContext( miwConfigBuilder, selfDescriptionService ); + var dataOfferPageApiService = new DataOfferPageApiService(); var uiResource = new UiResourceImpl( contractAgreementApiService, contractAgreementTransferApiService, @@ -264,7 +266,8 @@ public static WrapperExtensionContext buildContext( contractDefinitionApiService, contractNegotiationApiService, dashboardApiService, - dslContextFactory + dslContextFactory, + dataOfferPageApiService ); // Use Case API diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 158d8f559..1d7af06fe 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; @@ -41,15 +42,12 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_negotiations.ContractNegotiationApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.DashboardPageApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.data_offer.DataOfferPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageApiService; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferHistoryPageAssetFetcherService; import de.sovity.edc.extension.db.directaccess.DslContextFactory; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validation; -import jakarta.validation.ValidatorFactory; import lombok.RequiredArgsConstructor; -import lombok.val; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -72,6 +70,7 @@ public class UiResourceImpl implements UiResource { private final ContractNegotiationApiService contractNegotiationApiService; private final DashboardPageApiService dashboardPageApiService; private final DslContextFactory dslContextFactory; + private final DataOfferPageApiService dataOfferPageApiService; @Override public DashboardPage getDashboardPage() { @@ -178,4 +177,22 @@ public TransferHistoryPage getTransferHistoryPage() { public UiAsset getTransferProcessAsset(String transferProcessId) { return transferHistoryPageAssetFetcherService.getAssetForTransferHistoryPage(transferProcessId); } + + @Override + public IdAvailabilityResponse isPolicyIdAvailable(String policyId) { + return dslContextFactory.transactionResult(dsl -> + dataOfferPageApiService.checkIfPolicyIdAvailable(dsl, policyId)); + } + + @Override + public IdAvailabilityResponse isAssetIdAvailable(String assetId) { + return dslContextFactory.transactionResult(dsl -> + dataOfferPageApiService.checkIfAssetIdAvailable(dsl, assetId)); + } + + @Override + public IdAvailabilityResponse isContractDefinitionIdAvailable(String contractDefinitionId) { + return dslContextFactory.transactionResult(dsl -> + dataOfferPageApiService.checkIfContractDefinitionIdAvailable(dsl, contractDefinitionId)); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java new file mode 100644 index 000000000..43137206a --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java @@ -0,0 +1,45 @@ +package de.sovity.edc.ext.wrapper.api.ui.pages.data_offer; + +import de.sovity.edc.ext.db.jooq.Tables; +import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.jooq.Table; +import org.jooq.TableField; + +@RequiredArgsConstructor +public class DataOfferPageApiService { + @NotNull + public IdAvailabilityResponse checkIfPolicyIdAvailable(DSLContext dsl, String id) { + val table = Tables.EDC_POLICYDEFINITIONS; + val field = table.POLICY_ID; + + return new IdAvailabilityResponse(id, isIdAvailable(dsl, table, field, id)); + } + + @NotNull + public IdAvailabilityResponse checkIfAssetIdAvailable(DSLContext dsl, String id) { + val table = Tables.EDC_ASSET; + val field = table.ASSET_ID; + + return new IdAvailabilityResponse(id, isIdAvailable(dsl, table, field, id)); + } + + @NotNull + public IdAvailabilityResponse checkIfContractDefinitionIdAvailable(DSLContext dsl, String id) { + val table = Tables.EDC_CONTRACT_DEFINITIONS; + val field = table.CONTRACT_DEFINITION_ID; + + return new IdAvailabilityResponse(id, isIdAvailable(dsl, table, field, id)); + } + + private boolean isIdAvailable(DSLContext dsl, Table table, TableField idField, String id) { + return !dsl.fetchExists( + dsl.select(idField) + .from(table) + .where(idField.eq(id)) + ); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index 595619c5d..a20f44b0c 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -209,6 +209,7 @@ void limitTheReasonSizeAt100Chars( // termination completed == success } + @DisabledOnGithub @Test void limitTheDetailSizeAt1000Chars( E2eScenario scenario, diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index f16aa4d7b..9c4e79360 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -43,6 +43,7 @@ import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; @@ -102,82 +103,82 @@ void provide_consume_assetMapping_policyMapping_agreements( var yesterday = OffsetDateTime.now().minusDays(1); var constraintRequest = UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.GT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(yesterday.toString()) - .build()) - .build(); + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(yesterday.toString()) + .build()) + .build(); var policyId = providerClient.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() - .policyDefinitionId("policy-1") - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(constraintRequest)) - .build()) - .build()).getId(); + .policyDefinitionId("policy-1") + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of(constraintRequest)) + .build()) + .build()).getId(); var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl(dataAddress.getDataSourceUrl(data)) - .build()) - .build(); + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(data)) + .build()) + .build(); var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() - .id("asset-1") - .title("AssetName") - .description("AssetDescription") - .licenseUrl("https://license-url") - .version("1.0.0") - .language("en") - .mediaType("application/json") - .dataCategory("dataCategory") - .dataSubcategory("dataSubcategory") - .dataModel("dataModel") - .geoReferenceMethod("geoReferenceMethod") - .transportMode("transportMode") - .sovereignLegalName("my-sovereign") - .geoLocation("my-geolocation") - .nutsLocations(Arrays.asList("my-nuts-location1", "my-nuts-location2")) - .dataSampleUrls(Arrays.asList("my-data-sample-urls1", "my-data-sample-urls2")) - .referenceFileUrls(Arrays.asList("my-reference-files1", "my-reference-files2")) - .referenceFilesDescription("my-additional-description") - .conditionsForUse("my-conditions-for-use") - .dataUpdateFrequency("my-data-update-frequency") - .temporalCoverageFrom(LocalDate.parse("2007-12-03")) - .temporalCoverageToInclusive(LocalDate.parse("2024-01-22")) - .keywords(List.of("keyword1", "keyword2")) - .publisherHomepage("publisherHomepage") - .dataSource(dataSource) - .customJsonAsString(""" - {"test": "value"} - """) - .customJsonLdAsString(""" - {"https://public/some#key": "public LD value"} - """) - .privateCustomJsonAsString(""" - {"private_test": "private value"} - """) - .privateCustomJsonLdAsString(""" - {"https://private/some#key": "private LD value"} - """) - .build()).getId(); + .id("asset-1") + .title("AssetName") + .description("AssetDescription") + .licenseUrl("https://license-url") + .version("1.0.0") + .language("en") + .mediaType("application/json") + .dataCategory("dataCategory") + .dataSubcategory("dataSubcategory") + .dataModel("dataModel") + .geoReferenceMethod("geoReferenceMethod") + .transportMode("transportMode") + .sovereignLegalName("my-sovereign") + .geoLocation("my-geolocation") + .nutsLocations(Arrays.asList("my-nuts-location1", "my-nuts-location2")) + .dataSampleUrls(Arrays.asList("my-data-sample-urls1", "my-data-sample-urls2")) + .referenceFileUrls(Arrays.asList("my-reference-files1", "my-reference-files2")) + .referenceFilesDescription("my-additional-description") + .conditionsForUse("my-conditions-for-use") + .dataUpdateFrequency("my-data-update-frequency") + .temporalCoverageFrom(LocalDate.parse("2007-12-03")) + .temporalCoverageToInclusive(LocalDate.parse("2024-01-22")) + .keywords(List.of("keyword1", "keyword2")) + .publisherHomepage("publisherHomepage") + .dataSource(dataSource) + .customJsonAsString(""" + {"test": "value"} + """) + .customJsonLdAsString(""" + {"https://public/some#key": "public LD value"} + """) + .privateCustomJsonAsString(""" + {"private_test": "private value"} + """) + .privateCustomJsonLdAsString(""" + {"https://private/some#key": "private LD value"} + """) + .build()).getId(); assertThat(assetId).isEqualTo("asset-1"); providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() - .contractDefinitionId("cd-1") - .accessPolicyId(policyId) - .contractPolicyId(policyId) - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(assetId) - .build()) - .build())) - .build()); + .contractDefinitionId("cd-1") + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId) + .build()) + .build())) + .build()); var assets = providerClient.uiApi().getAssetPage().getAssets(); assertThat(assets).hasSize(1); @@ -232,11 +233,11 @@ void provide_consume_assetMapping_policyMapping_agreements( assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyQueryParams()).isFalse(); assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyBody()).isFalse(); assertThatJson(dataOffer.getAsset().getCustomJsonAsString()).isEqualTo(""" - {"test": "value"} - """); + {"test": "value"} + """); assertThatJson(dataOffer.getAsset().getCustomJsonLdAsString()).isEqualTo(""" - {"https://public/some#key":"public LD value"} - """); + {"https://public/some#key":"public LD value"} + """); assertThat(dataOffer.getAsset().getPrivateCustomJsonAsString()).isNullOrEmpty(); assertThatJson(dataOffer.getAsset().getPrivateCustomJsonLdAsString()).isObject().isEmpty(); @@ -247,17 +248,17 @@ void provide_consume_assetMapping_policyMapping_agreements( assertThat(asset.getParticipantId()).isEqualTo(providerConfig.getProperties().get("edc.participant.id")); assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" - { "test": "value" } - """); + { "test": "value" } + """); assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" - { "https://public/some#key": "public LD value" } - """); + { "https://public/some#key": "public LD value" } + """); assertThatJson(asset.getPrivateCustomJsonAsString()).isEqualTo(""" - { "private_test": "private value" } - """); + { "private_test": "private value" } + """); assertThatJson(asset.getPrivateCustomJsonLdAsString()).isEqualTo(""" - { "https://private/some#key": "private LD value" } - """); + { "https://private/some#key": "private LD value" } + """); // Contract Agreement assertThat(providerAgreements).hasSize(1); @@ -313,26 +314,26 @@ void provide_consume_assetMapping_policyMapping_agreements( void canOverrideTheWellKnowPropertiesUsingTheCustomProperties(@Provider EdcClient providerClient) { // arrange var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl("http://example.com/base") - .build()) - .build(); + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com/base") + .build()) + .build(); var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() - .id("asset-1") - .title("will be overridden") - .dataSource(dataSource) - .customJsonLdAsString(""" - { - "http://purl.org/dc/terms/title": "The real title", - "http://purl.org/dc/terms/spatial": { - "http://purl.org/dc/terms/identifier": ["a", "b", "c"] - }, - "http://example.com/an-actual-custom-property": "custom value" - } - """) - .build()).getId(); + .id("asset-1") + .title("will be overridden") + .dataSource(dataSource) + .customJsonLdAsString(""" + { + "http://purl.org/dc/terms/title": "The real title", + "http://purl.org/dc/terms/spatial": { + "http://purl.org/dc/terms/identifier": ["a", "b", "c"] + }, + "http://example.com/an-actual-custom-property": "custom value" + } + """) + .build()).getId(); assertThat(assetId).isEqualTo("asset-1"); // act @@ -350,10 +351,10 @@ void canOverrideTheWellKnowPropertiesUsingTheCustomProperties(@Provider EdcClien assertThat(asset.getNutsLocations()).isEqualTo(List.of("a", "b", "c")); // remaining custom property assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" - { - "http://example.com/an-actual-custom-property": "custom value" - } - """); + { + "http://example.com/an-actual-custom-property": "custom value" + } + """); } @DisabledOnGithub @@ -368,31 +369,31 @@ void customTransferRequest( var data = "expected data 123"; var dataSource = UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .baseUrl(dataAddress.getDataSourceUrl(data)) - .build()) - .build(); + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .baseUrl(dataAddress.getDataSourceUrl(data)) + .build()) + .build(); var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() - .id("asset-1") - .dataSource(dataSource) - .build()).getId(); + .id("asset-1") + .dataSource(dataSource) + .build()).getId(); assertThat(assetId).isEqualTo("asset-1"); var policyId = providerClient.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() - .policyDefinitionId("policy-1") - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) - .build()) - .build()).getId(); + .policyDefinitionId("policy-1") + .policy(UiPolicyCreateRequest.builder() + .constraints(List.of()) + .build()) + .build()).getId(); providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() - .contractDefinitionId("cd-1") - .accessPolicyId(policyId) - .contractPolicyId(policyId) - .assetSelector(List.of()) - .build()); + .contractDefinitionId("cd-1") + .accessPolicyId(policyId) + .contractPolicyId(policyId) + .assetSelector(List.of()) + .build()); val providerProtocolEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint); @@ -404,21 +405,21 @@ void customTransferRequest( // act var negotiation = negotiate(consumerClient, consumerConnector, dataOffer, contractOffer); var transferRequestJsonLd = Json.createObjectBuilder() - .add( - Prop.Edc.DATA_DESTINATION, - getDatasinkPropertiesJsonObject() - ) - .add(Prop.Edc.CTX + "transferType", Json.createObjectBuilder() - .add(Prop.Edc.CTX + "contentType", "application/octet-stream") - .add(Prop.Edc.CTX + "isFinite", true) - ) - .add(Prop.Edc.CTX + "protocol", HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) - .add(Prop.Edc.CTX + "managedResources", false) - .build(); + .add( + Prop.Edc.DATA_DESTINATION, + getDatasinkPropertiesJsonObject() + ) + .add(Prop.Edc.CTX + "transferType", Json.createObjectBuilder() + .add(Prop.Edc.CTX + "contentType", "application/octet-stream") + .add(Prop.Edc.CTX + "isFinite", true) + ) + .add(Prop.Edc.CTX + "protocol", HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP) + .add(Prop.Edc.CTX + "managedResources", false) + .build(); var transferRequest = InitiateCustomTransferRequest.builder() - .contractAgreementId(negotiation.getContractAgreementId()) - .transferProcessRequestJsonLd(JsonUtils.toJson(transferRequestJsonLd)) - .build(); + .contractAgreementId(negotiation.getContractAgreementId()) + .transferProcessRequestJsonLd(JsonUtils.toJson(transferRequestJsonLd)) + .build(); consumerClient.uiApi().initiateCustomTransfer(transferRequest); validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); @@ -443,48 +444,48 @@ void editAssetOnLiveContract( .build(); var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() - .id("asset-1") - .title("Bad Asset Title") - .dataSource(dataSource) - .customJsonAsString(""" - { - "test": "value" - } - """) - .customJsonLdAsString(""" - { - "test": "not a valid key, will be deleted", - "http://example.com/key-to-delete": "with a valida key", - "http://example.com/key-to-edit": "with a valida key" - } - """) - .privateCustomJsonAsString(""" - { - "private-test": "value" - } - """) - .privateCustomJsonLdAsString(""" - { - "private-test": "not a valid key, will be deleted", - "http://example.com/private-key-to-delete": "private with a valid key", - "http://example.com/private-key-to-edit": "private with a valid key" - } - """) - .build()).getId(); + .id("asset-1") + .title("Bad Asset Title") + .dataSource(dataSource) + .customJsonAsString(""" + { + "test": "value" + } + """) + .customJsonLdAsString(""" + { + "test": "not a valid key, will be deleted", + "http://example.com/key-to-delete": "with a valida key", + "http://example.com/key-to-edit": "with a valida key" + } + """) + .privateCustomJsonAsString(""" + { + "private-test": "value" + } + """) + .privateCustomJsonLdAsString(""" + { + "private-test": "not a valid key, will be deleted", + "http://example.com/private-key-to-delete": "private with a valid key", + "http://example.com/private-key-to-edit": "private with a valid key" + } + """) + .build()).getId(); providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() - .contractDefinitionId("cd-1") - .accessPolicyId("always-true") - .contractPolicyId("always-true") - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(assetId) - .build()) - .build())) - .build()); + .contractDefinitionId("cd-1") + .accessPolicyId("always-true") + .contractPolicyId("always-true") + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(assetId) + .build()) + .build())) + .build()); val providerProtocolEndpoint = providerConfig.getProtocolEndpoint().getUri().toString(); var dataOffers = consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint); @@ -496,67 +497,95 @@ void editAssetOnLiveContract( // act providerClient.uiApi().editAsset(assetId, UiAssetEditRequest.builder() - .title("Good Asset Title") - .customJsonAsString(""" - { - "edited": "new value" - } - """) - .customJsonLdAsString(""" - { - "edited": "not a valid key, will be deleted", - "http://example.com/key-to-delete": null, - "http://example.com/key-to-edit": "with a valid key", - "http://example.com/extra": "value to add" - } - """) - .privateCustomJsonAsString(""" - { - "private-edited": "new value" - } - """) - .privateCustomJsonLdAsString(""" - { - "private-edited": "not a valid key, will be deleted", - "http://example.com/private-key-to-delete": null, - "http://example.com/private-key-to-edit": "private with a valid key", - "http://example.com/private-extra": "private value to add" - } - """) - .build()); - initiateTransfer(consumerClient, negotiation); - - // assert - assertThat(consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); - val firstAsset = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements().get(0).getAsset(); - assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); - assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" + .title("Good Asset Title") + .customJsonAsString(""" { "edited": "new value" } - """); - assertThatJson(firstAsset.getCustomJsonLdAsString()).isEqualTo(""" + """) + .customJsonLdAsString(""" { + "edited": "not a valid key, will be deleted", + "http://example.com/key-to-delete": null, "http://example.com/key-to-edit": "with a valid key", "http://example.com/extra": "value to add" } - """); - assertThat(firstAsset.getPrivateCustomJsonAsString()).isEqualTo(""" + """) + .privateCustomJsonAsString(""" { "private-edited": "new value" } - """); - assertThatJson(firstAsset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + """) + .privateCustomJsonLdAsString(""" { + "private-edited": "not a valid key, will be deleted", + "http://example.com/private-key-to-delete": null, "http://example.com/private-key-to-edit": "private with a valid key", "http://example.com/private-extra": "private value to add" } - """); + """) + .build()); + initiateTransfer(consumerClient, negotiation); + + // assert + assertThat(consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); + val firstAsset = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements().get(0).getAsset(); + assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); + assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" + { + "edited": "new value" + } + """); + assertThatJson(firstAsset.getCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/key-to-edit": "with a valid key", + "http://example.com/extra": "value to add" + } + """); + assertThat(firstAsset.getPrivateCustomJsonAsString()).isEqualTo(""" + { + "private-edited": "new value" + } + """); + assertThatJson(firstAsset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/private-key-to-edit": "private with a valid key", + "http://example.com/private-extra": "private value to add" + } + """); validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); validateTransferProcessesOk(consumerClient, providerClient); assertThat(providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0).getAssetName()).isEqualTo("Good Asset Title"); } + @Test + void checkIdAvailability(E2eScenario scenario, @Provider EdcClient providerClient) { + // arrange + var assetId = scenario.createAsset(); + var policyId = "policy-id"; + scenario.createPolicy(policyId, OffsetDateTime.MIN, OffsetDateTime.MAX); + var contractDefinitionId = scenario.createContractDefinition(policyId, assetId); + + val asset = providerClient.uiApi().getAssetPage(); + + // act + val negAssetResponse = providerClient.uiApi().isAssetIdAvailable(assetId); + val negPolicyResponse = providerClient.uiApi().isPolicyIdAvailable(policyId); + val negContractDefinitionResponse = providerClient.uiApi().isContractDefinitionIdAvailable(contractDefinitionId.getId()); + val posAssetResponse = providerClient.uiApi().isAssetIdAvailable("new-asset"); + val posPolicyResponse = providerClient.uiApi().isPolicyIdAvailable("new-policy"); + val posContractDefinitionResponse = providerClient.uiApi().isContractDefinitionIdAvailable("new-cd"); + + // assert + assertThat(negAssetResponse.getAvailable()).isFalse(); + assertThat(negPolicyResponse.getAvailable()).isFalse(); + assertThat(negContractDefinitionResponse.getAvailable()).isFalse(); + + assertThat(posAssetResponse.getAvailable()).isTrue(); + assertThat(posPolicyResponse.getAvailable()).isTrue(); + assertThat(posContractDefinitionResponse.getAvailable()).isTrue(); + } + private UiContractNegotiation negotiate( EdcClient consumerClient, ConnectorRemote consumerConnector, @@ -564,19 +593,19 @@ private UiContractNegotiation negotiate( UiContractOffer contractOffer) { var negotiationRequest = ContractNegotiationRequest.builder() - .counterPartyAddress(dataOffer.getEndpoint()) - .counterPartyParticipantId(dataOffer.getParticipantId()) - .assetId(dataOffer.getAsset().getAssetId()) - .contractOfferId(contractOffer.getContractOfferId()) - .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) - .build(); + .counterPartyAddress(dataOffer.getEndpoint()) + .counterPartyParticipantId(dataOffer.getParticipantId()) + .assetId(dataOffer.getAsset().getAssetId()) + .contractOfferId(contractOffer.getContractOfferId()) + .policyJsonLd(contractOffer.getPolicy().getPolicyJsonLd()) + .build(); var negotiationId = consumerClient.uiApi().initiateContractNegotiation(negotiationRequest) - .getContractNegotiationId(); + .getContractNegotiationId(); var negotiation = Awaitility.await().atMost(consumerConnector.timeout).until( - () -> consumerClient.uiApi().getContractNegotiation(negotiationId), - it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS + () -> consumerClient.uiApi().getContractNegotiation(negotiationId), + it -> it.getState().getSimplifiedState() != ContractNegotiationSimplifiedState.IN_PROGRESS ); assertThat(negotiation.getState().getSimplifiedState()).isEqualTo(ContractNegotiationSimplifiedState.AGREED); @@ -586,9 +615,9 @@ private UiContractNegotiation negotiate( private void initiateTransfer(EdcClient consumerClient, UiContractNegotiation negotiation) { var contractAgreementId = negotiation.getContractAgreementId(); var transferRequest = InitiateTransferRequest.builder() - .contractAgreementId(contractAgreementId) - .dataSinkProperties(dataAddress.getDataSinkProperties()) - .build(); + .contractAgreementId(contractAgreementId) + .dataSinkProperties(dataAddress.getDataSinkProperties()) + .build(); consumerClient.uiApi().initiateTransfer(transferRequest); } From f063fd00c8d322ad4958502767d93a744087f019 Mon Sep 17 00:00:00 2001 From: Ilia Orlov <66363651+illfixit@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:18:11 +0200 Subject: [PATCH 267/295] feat: extend UiPolicy from only constraints to expressions (#988) Co-authored-by: Richard Treier --- CHANGELOG.md | 4 + docs/api/sovity-edc-api-wrapper.yaml | 194 ++++++-------- .../ext/catalog/crawler/CrawlerE2eTest.java | 24 +- .../CrawlerExtensionContextBuilder.java | 171 ++++++------ .../wrapper/clients/java-client/README.md | 94 ++++--- .../edc/ext/wrapper/api/ui/UiResource.java | 14 +- .../ui/model/PolicyDefinitionCreateDto.java} | 11 +- .../model/PolicyDefinitionCreateRequest.java} | 17 +- .../api/ui}/model/PolicyDefinitionDto.java | 3 +- .../api/ui/model/PolicyDefinitionPage.java | 1 - .../wrapper/api/usecase/UseCaseResource.java | 9 - .../wrapper/api/common/model/AssetDto.java | 42 --- .../api/common/model/AtomicConstraintDto.java | 44 --- .../wrapper/api/common/model/Expression.java | 47 ---- .../api/common/model/ExpressionType.java | 17 -- .../wrapper/api/common/model/OperatorDto.java | 7 +- .../api/common/model/PermissionDto.java | 36 --- .../ext/wrapper/api/common/model/UiAsset.java | 18 +- .../common/model/UiAssetCreateRequest.java | 2 +- .../api/common/model/UiAssetEditRequest.java | 18 +- .../api/common/model/UiDataSource.java | 2 +- .../common/model/UiDataSourceHttpData.java | 1 + .../model/UiDataSourceHttpDataMethod.java | 2 +- .../wrapper/api/common/model/UiPolicy.java | 12 +- .../api/common/model/UiPolicyConstraint.java | 2 +- .../common/model/UiPolicyCreateRequest.java | 7 +- .../api/common/model/UiPolicyExpression.java | 66 +++++ .../common/model/UiPolicyExpressionType.java | 20 ++ .../api/common/model/UiPolicyLiteral.java | 18 +- .../api/common/mappers/AssetMapper.java | 2 +- .../common/mappers/LegacyPolicyMapper.java | 54 ++++ .../api/common/mappers/PolicyMapper.java | 91 ++----- .../mappers/asset/utils/JsonBuilderUtils.java | 4 +- .../dataaddress/http/HttpHeaderMapper.java | 2 +- .../policy/AtomicConstraintMapper.java | 67 ++--- .../mappers/policy/ConstraintExtractor.java | 114 -------- .../mappers/policy/ExpressionExtractor.java | 97 +++++++ .../mappers/policy/ExpressionMapper.java | 144 ++++++++++ .../common/mappers/policy/LiteralMapper.java | 16 +- .../mappers/LegacyPolicyMapperTest.java | 105 ++++++++ .../api/common/mappers/PolicyMapperTest.java | 125 ++++----- .../policy/AtomicConstraintMapperTest.java | 22 +- .../policy/ConstraintExtractorTest.java | 103 ------- .../policy/ExpressionExtractorTest.java | 211 +++++++++++++++ .../mappers/policy/ExpressionMapperTest.java | 251 ++++++++++++++++++ .../WrapperExtensionContextBuilder.java | 38 ++- .../ext/wrapper/api/ui/UiResourceImpl.java | 9 +- .../policy/PolicyDefinitionApiService.java | 58 ++-- .../api/usecase/UseCaseResourceImpl.java | 9 - .../api/ui/pages/catalog/CatalogApiTest.java | 13 +- .../ContractAgreementPageTest.java | 6 +- .../PolicyDefinitionApiServiceTest.java | 32 ++- .../PolicyDefinitionApiServiceTest.java | 125 --------- .../api/usecase/UseCaseApiWrapperTest.java | 13 +- .../de/sovity/edc/e2e/ApiWrapperDemoTest.java | 48 ++-- .../e2e/DataSourceParameterizationTest.java | 14 +- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 44 +-- .../sovity/edc/e2e/UseCaseApiWrapperTest.java | 98 ++++--- .../extension/e2e/extension/E2eScenario.java | 43 ++- 59 files changed, 1624 insertions(+), 1237 deletions(-) rename extensions/wrapper/{wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java => wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java} (70%) rename extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/{usecase/model/PolicyCreateRequest.java => ui/model/PolicyDefinitionCreateRequest.java} (62%) rename extensions/wrapper/{wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common => wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui}/model/PolicyDefinitionDto.java (90%) delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java delete mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpression.java create mode 100644 extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpressionType.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapper.java delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractor.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapper.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapperTest.java delete mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractorTest.java create mode 100644 extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapperTest.java delete mode 100644 extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eac36b22..de3b23884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Major Changes +- The `UiPolicy` model has been adjusted to support complex expressions including `AND`, `OR` and `XONE`. + - The `createPolicyDefinition` has been marked as deprecated in favor of the new `createPolicyDefinitionV2` endpoint that supports complex policies. + - Removed the recently rushed `createPolicyDefinitionUseCase` endpoint in favor of the new `createPolicyDefinitionV2` endpoint. + #### Minor Changes - Both providers and consumers can now terminate their contracts. diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index c466cbb97..3f9ca3a7f 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -121,7 +121,8 @@ paths: post: tags: - UI - description: Create a new Policy Definition + description: "[Deprecated] Create a new Policy Definition from a list of constraints.\ + \ Use createPolicyDefinitionV2 instead." operationId: createPolicyDefinition requestBody: content: @@ -135,6 +136,25 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' + deprecated: true + /wrapper/ui/v2/pages/policy-page/policy-definitions: + post: + tags: + - UI + description: Create a new Policy Definition + operationId: createPolicyDefinitionV2 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyDefinitionCreateDto' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' /wrapper/ui/pages/asset-page/assets/{assetId}: put: tags: @@ -495,24 +515,6 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' - /wrapper/use-case-api/policy-definition: - post: - tags: - - Use Case - description: Create a new Policy Definition - operationId: createPolicyDefinitionUseCase - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyCreateRequest' - responses: - default: - description: default response - content: - application/json: - schema: - $ref: '#/components/schemas/IdResponseDto' /wrapper/use-case-api/kpis: get: tags: @@ -582,11 +584,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource + default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM - default: CUSTOM SecretValue: type: object properties: @@ -719,8 +721,8 @@ components: type: string description: Same as customJsonLdAsString but the data will be stored in the private properties. The same limitations apply. - description: Type-Safe OpenAPI generator friendly Asset Create DTO that supports - an opinionated subset of the original EDC Asset Entity. + description: Type-safe data offer metadata for creating an asset as supported + by the sovity product landscape. Contains extension points. UiDataSource: required: - type @@ -738,8 +740,8 @@ components: type: string description: For all types. Custom Data Address Properties. description: For all types. Custom Data Address Properties. - description: Data Offer Data Source Model. Supports certain Data Address types - but also leaves a backdoor for custom Data Address Properties. + description: Type-safe data source as supported by the sovity product landscape. + Contains extension points for using custom data address properties. UiDataSourceHttpData: required: - baseUrl @@ -790,10 +792,11 @@ components: \ both a request body and a content type. Only Methods POST, PUT and PATCH\ \ allow request bodies." default: false - description: Only for type HTTP_DATA + description: HTTP_DATA type Data Source. UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource + default: GET enum: - GET - POST @@ -801,7 +804,6 @@ components: - PATCH - DELETE - OPTIONS - default: GET UiDataSourceOnRequest: required: - contactEmail @@ -899,7 +901,8 @@ components: - LIKE OperatorDto: type: string - description: Operator for policies + description: Type-Safe ODRL Policy Operator as supported by the sovity product + landscape enum: - EQ - NEQ @@ -924,7 +927,8 @@ components: description: Policy Definition ID policy: $ref: '#/components/schemas/UiPolicyCreateRequest' - description: Data for creating a Policy Definition + description: "[Deprecated] Create a Policy Definition. Use PolicyDefinitionCreateDto" + deprecated: true UiPolicyConstraint: required: - left @@ -939,18 +943,19 @@ components: $ref: '#/components/schemas/OperatorDto' right: $ref: '#/components/schemas/UiPolicyLiteral' - description: ODRL AtomicConstraint as supported by our UI + description: "ODRL AtomicConstraint as supported by the sovity product landscape.\ + \ For example 'a EQ b', 'c IN [d, e, f]'" UiPolicyCreateRequest: type: object properties: constraints: type: array - description: Conjunction of required expressions for the policy to evaluate - to TRUE. + description: Conjunction of required constraints + deprecated: true items: $ref: '#/components/schemas/UiPolicyConstraint' - description: Type-Safe OpenAPI generator friendly Policy Create DTO that supports - an opinionated subset of the original EDC Policy Entity. + description: "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)" + deprecated: true UiPolicyLiteral: required: - type @@ -975,6 +980,48 @@ components: - STRING - STRING_LIST - JSON + PolicyDefinitionCreateDto: + required: + - expression + - policyDefinitionId + type: object + properties: + policyDefinitionId: + type: string + description: Policy Definition ID + expression: + $ref: '#/components/schemas/UiPolicyExpression' + description: Create a Policy Definition + UiPolicyExpression: + required: + - type + type: object + properties: + type: + $ref: '#/components/schemas/UiPolicyExpressionType' + expressions: + type: array + description: "Only for types AND, OR, XONE. List of sub-expressions to be\ + \ evaluated according to the expressionType." + items: + $ref: '#/components/schemas/UiPolicyExpression' + constraint: + $ref: '#/components/schemas/UiPolicyConstraint' + description: ODRL constraint as supported by the sovity product landscape + UiPolicyExpressionType: + type: string + description: | + Ui Policy Expression types: + * `CONSTRAINT` - Expression 'a=b' + * `AND` - Conjunction of several expressions. Evaluates to true iff all child expressions are true. + * `OR` - Disjunction of several expressions. Evaluates to true iff at least one child expression is true. + * `XONE` - XONE operation. Evaluates to true iff exactly one child expression is true. + enum: + - EMPTY + - CONSTRAINT + - AND + - OR + - XONE UiAssetEditRequest: type: object properties: @@ -1087,7 +1134,8 @@ components: type: string description: Same as customJsonLdAsString but the data will be stored in the private properties. The same limitations apply. - description: Data for editing an asset. + description: Type-safe data offer metadata for editing an asset as supported + by the sovity product landscape. Contains extension points. AssetPage: required: - assets @@ -1274,7 +1322,8 @@ components: type: string description: Same as customJsonLdAsString but the data will be stored in the private properties. The same limitations apply. - description: Type-Safe Asset Metadata as needed by our UI + description: Type-safe data offer metadata as supported by the sovity product + landscape. Contains extension points. UiContractOffer: required: - contractOfferId @@ -1319,12 +1368,8 @@ components: type: string description: EDC Policy JSON-LD. This is required because the EDC requires the full policy when initiating contract negotiations. - constraints: - type: array - description: Conjunction of required expressions for the policy to evaluate - to TRUE. - items: - $ref: '#/components/schemas/UiPolicyConstraint' + expression: + $ref: '#/components/schemas/UiPolicyExpression' errors: type: array description: "When trying to reduce the policy JSON-LD to our opinionated\ @@ -1337,8 +1382,8 @@ components: \ subset of functionalities, many fields and functionalities are unsupported.\ \ Should any discrepancies occur during the mapping process, we'll collect\ \ them here." - description: Type-Safe OpenAPI generator friendly Policy DTO as needed by our - UI + description: Type-Safe OpenAPI generator friendly ODLR policy subset as endorsed + by sovity. ContractAgreementCard: required: - asset @@ -1872,69 +1917,6 @@ components: type: string description: A short reason why this contract was terminated description: Data for terminating a Contract Agreement - AtomicConstraintDto: - required: - - leftExpression - - operator - - rightExpression - type: object - properties: - leftExpression: - type: string - description: Left part of the constraint. - operator: - $ref: '#/components/schemas/OperatorDto' - rightExpression: - type: string - description: Right part of the constraint. - description: Type-Safe OpenAPI generator friendly Constraint DTO that supports - an opinionated subset of the original EDC Constraint Entity. - Expression: - type: object - properties: - expressionType: - $ref: '#/components/schemas/ExpressionType' - expressions: - type: array - description: List of policy elements that are evaluated according the expressionType. - items: - $ref: '#/components/schemas/Expression' - atomicConstraint: - $ref: '#/components/schemas/AtomicConstraintDto' - description: Represents a single atomic constraint or a multiplicity constraint. - The atomicConstraint will be evaluated if the constraintType is ATOMIC_CONSTRAINT. - ExpressionType: - type: string - description: | - Expression types: - * `ATOMIC_CONSTRAINT` - A single constraint for the policy - * `AND` - Several constraints, all of which must be respected - * `OR` - Several constraints, of which at least one must be respected - * `XOR` - Several constraints, of which exactly one must be respected - enum: - - ATOMIC_CONSTRAINT - - AND - - OR - - XOR - PermissionDto: - required: - - expression - type: object - properties: - expression: - $ref: '#/components/schemas/Expression' - description: Permission description for the policy to evaluate to TRUE. - PolicyCreateRequest: - required: - - policyDefinitionId - type: object - properties: - policyDefinitionId: - type: string - description: Policy Definition ID - permission: - $ref: '#/components/schemas/PermissionDto' - description: Policy Creation Request Supporting Multiplicity Constraints. KpiResult: required: - assetsCount diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java index db8a7631e..759f65cf4 100644 --- a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java @@ -18,7 +18,7 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; @@ -27,7 +27,8 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.ext.catalog.crawler.dao.connectors.ConnectorRef; @@ -49,6 +50,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Stream; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.getFreePortRange; @@ -191,14 +193,22 @@ private void createPolicy() { .build()) .build(); - var policyDefinition = PolicyDefinitionCreateRequest.builder() + var expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(Stream.of(afterYesterday, beforeTomorrow) + .map(it -> UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(it) + .build()) + .toList()) + .build(); + + var policyDefinition = PolicyDefinitionCreateDto.builder() .policyDefinitionId(dataOfferId) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(afterYesterday, beforeTomorrow)) - .build()) + .expression(expression) .build(); - connectorClient.uiApi().createPolicyDefinition(policyDefinition); + connectorClient.uiApi().createPolicyDefinitionV2(policyDefinition); } private void createContractDefinition() { diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java index 2437dd751..f95463a9f 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java @@ -63,7 +63,8 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; @@ -95,12 +96,12 @@ public class CrawlerExtensionContextBuilder { public static CrawlerExtensionContext buildContext( - Config config, - Monitor monitor, - TypeManager typeManager, - TypeTransformerRegistry typeTransformerRegistry, - JsonLd jsonLd, - CatalogService catalogService + Config config, + Monitor monitor, + TypeManager typeManager, + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd, + CatalogService catalogService ) { // Config var crawlerConfigFactory = new CrawlerConfigFactory(config); @@ -124,9 +125,9 @@ public static CrawlerExtensionContext buildContext( var crawlerEventLogger = new CrawlerEventLogger(); var crawlerExecutionTimeLogger = new CrawlerExecutionTimeLogger(); var dataOfferMappingUtils = new FetchedCatalogMappingUtils( - policyMapper, - assetMapper, - objectMapperJsonLd + policyMapper, + assetMapper, + objectMapperJsonLd ); var contractOfferRecordUpdater = new ContractOfferRecordUpdater(); var shortDescriptionBuilder = new ShortDescriptionBuilder(); @@ -134,34 +135,34 @@ public static CrawlerExtensionContext buildContext( var contractOfferQueries = new ContractOfferQueries(); var dataOfferLimitsEnforcer = new DataOfferLimitsEnforcer(crawlerConfig, crawlerEventLogger); var dataOfferPatchBuilder = new CatalogPatchBuilder( - contractOfferQueries, - dataOfferQueries, - dataOfferRecordUpdater, - contractOfferRecordUpdater + contractOfferQueries, + dataOfferQueries, + dataOfferRecordUpdater, + contractOfferRecordUpdater ); var dataOfferPatchApplier = new CatalogPatchApplier(); var dataOfferWriter = new ConnectorUpdateCatalogWriter(dataOfferPatchBuilder, dataOfferPatchApplier); var connectorUpdateSuccessWriter = new ConnectorUpdateSuccessWriter( - crawlerEventLogger, - dataOfferWriter, - dataOfferLimitsEnforcer + crawlerEventLogger, + dataOfferWriter, + dataOfferLimitsEnforcer ); var fetchedDataOfferBuilder = new FetchedCatalogBuilder(dataOfferMappingUtils); var dspDataOfferBuilder = new DspDataOfferBuilder(jsonLd); var dspCatalogService = new DspCatalogService( - catalogService, - dspDataOfferBuilder + catalogService, + dspDataOfferBuilder ); var dataOfferFetcher = new FetchedCatalogService(dspCatalogService, fetchedDataOfferBuilder); var connectorUpdateFailureWriter = new ConnectorUpdateFailureWriter(crawlerEventLogger, monitor); var connectorUpdater = new ConnectorCrawler( - dataOfferFetcher, - connectorUpdateSuccessWriter, - connectorUpdateFailureWriter, - connectorQueries, - dslContextFactory, - monitor, - crawlerExecutionTimeLogger + dataOfferFetcher, + connectorUpdateSuccessWriter, + connectorUpdateFailureWriter, + connectorQueries, + dslContextFactory, + monitor, + crawlerExecutionTimeLogger ); var threadPoolTaskQueue = new ThreadPoolTaskQueue(); @@ -171,19 +172,19 @@ public static CrawlerExtensionContext buildContext( var connectorStatusUpdater = new ConnectorStatusUpdater(); var catalogCleaner = new CatalogCleaner(); var offlineConnectorCleaner = new OfflineConnectorCleaner( - crawlerConfig, - connectorQueries, - crawlerEventLogger, - connectorStatusUpdater, - catalogCleaner + crawlerConfig, + connectorQueries, + crawlerEventLogger, + connectorStatusUpdater, + catalogCleaner ); // Schedules List> jobs = List.of( - getOnlineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), - getOfflineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), - getDeadConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), - getOfflineConnectorCleanerCronJob(dslContextFactory, offlineConnectorCleaner) + getOnlineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getOfflineConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getDeadConnectorRefreshCronJob(dslContextFactory, connectorQueueFiller), + getOfflineConnectorCleanerCronJob(dslContextFactory, offlineConnectorCleaner) ); // Startup @@ -191,68 +192,70 @@ public static CrawlerExtensionContext buildContext( var crawlerInitializer = new CrawlerInitializer(quartzScheduleInitializer); return new CrawlerExtensionContext( - crawlerInitializer, - dataSource, - dslContextFactory, - connectorUpdater, - policyMapper, - fetchedDataOfferBuilder, - dataOfferRecordUpdater + crawlerInitializer, + dataSource, + dslContextFactory, + connectorUpdater, + policyMapper, + fetchedDataOfferBuilder, + dataOfferRecordUpdater ); } @NotNull - private static PolicyMapper newPolicyMapper(TypeTransformerRegistry typeTransformerRegistry, ObjectMapper objectMapperJsonLd) { + private static PolicyMapper newPolicyMapper( + TypeTransformerRegistry typeTransformerRegistry, + ObjectMapper objectMapperJsonLd + ) { var operatorMapper = new OperatorMapper(); - var literalMapper = new LiteralMapper( - objectMapperJsonLd - ); + var literalMapper = new LiteralMapper(objectMapperJsonLd); var atomicConstraintMapper = new AtomicConstraintMapper( - literalMapper, - operatorMapper + literalMapper, + operatorMapper ); var policyValidator = new PolicyValidator(); - var constraintExtractor = new ConstraintExtractor( - policyValidator, - atomicConstraintMapper + var expressionMapper = new ExpressionMapper(atomicConstraintMapper); + var constraintExtractor = new ExpressionExtractor( + policyValidator, + expressionMapper ); return new PolicyMapper( - constraintExtractor, - atomicConstraintMapper, - typeTransformerRegistry + constraintExtractor, + expressionMapper, + typeTransformerRegistry ); } @NotNull private static AssetMapper newAssetMapper( - TypeTransformerRegistry typeTransformerRegistry, - JsonLd jsonLd + TypeTransformerRegistry typeTransformerRegistry, + JsonLd jsonLd ) { var edcPropertyUtils = new EdcPropertyUtils(); var assetJsonLdUtils = new AssetJsonLdUtils(); var assetEditRequestMapper = new AssetEditRequestMapper(); var shortDescriptionBuilder = new ShortDescriptionBuilder(); var assetJsonLdParser = new AssetJsonLdParser( - assetJsonLdUtils, - shortDescriptionBuilder, - endpoint -> false + assetJsonLdUtils, + shortDescriptionBuilder, + endpoint -> false ); var httpHeaderMapper = new HttpHeaderMapper(); var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); var dataSourceMapper = new DataSourceMapper( - edcPropertyUtils, - httpDataSourceMapper + edcPropertyUtils, + httpDataSourceMapper ); var assetJsonLdBuilder = new AssetJsonLdBuilder( - dataSourceMapper, - assetJsonLdParser, - assetEditRequestMapper + dataSourceMapper, + assetJsonLdParser, + assetEditRequestMapper ); return new AssetMapper( - typeTransformerRegistry, - assetJsonLdBuilder, - assetJsonLdParser, - jsonLd + typeTransformerRegistry, + assetJsonLdBuilder, + assetJsonLdParser, + jsonLd ); } @@ -260,33 +263,33 @@ private static AssetMapper newAssetMapper( private static CronJobRef getOfflineConnectorCleanerCronJob(DslContextFactory dslContextFactory, OfflineConnectorCleaner offlineConnectorCleaner) { return new CronJobRef<>( - CrawlerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, - OfflineConnectorCleanerJob.class, - () -> new OfflineConnectorCleanerJob(dslContextFactory, offlineConnectorCleaner) + CrawlerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, + OfflineConnectorCleanerJob.class, + () -> new OfflineConnectorCleanerJob(dslContextFactory, offlineConnectorCleaner) ); } @NotNull private static CronJobRef getOnlineConnectorRefreshCronJob( - DslContextFactory dslContextFactory, - ConnectorQueueFiller connectorQueueFiller + DslContextFactory dslContextFactory, + ConnectorQueueFiller connectorQueueFiller ) { return new CronJobRef<>( - CrawlerExtension.CRON_ONLINE_CONNECTOR_REFRESH, - OnlineConnectorRefreshJob.class, - () -> new OnlineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + CrawlerExtension.CRON_ONLINE_CONNECTOR_REFRESH, + OnlineConnectorRefreshJob.class, + () -> new OnlineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) ); } @NotNull private static CronJobRef getOfflineConnectorRefreshCronJob( - DslContextFactory dslContextFactory, - ConnectorQueueFiller connectorQueueFiller + DslContextFactory dslContextFactory, + ConnectorQueueFiller connectorQueueFiller ) { return new CronJobRef<>( - CrawlerExtension.CRON_OFFLINE_CONNECTOR_REFRESH, - OfflineConnectorRefreshJob.class, - () -> new OfflineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + CrawlerExtension.CRON_OFFLINE_CONNECTOR_REFRESH, + OfflineConnectorRefreshJob.class, + () -> new OfflineConnectorRefreshJob(dslContextFactory, connectorQueueFiller) ); } @@ -294,9 +297,9 @@ private static CronJobRef getOfflineConnectorRefresh private static CronJobRef getDeadConnectorRefreshCronJob(DslContextFactory dslContextFactory, ConnectorQueueFiller connectorQueueFiller) { return new CronJobRef<>( - CrawlerExtension.CRON_DEAD_CONNECTOR_REFRESH, - DeadConnectorRefreshJob.class, - () -> new DeadConnectorRefreshJob(dslContextFactory, connectorQueueFiller) + CrawlerExtension.CRON_DEAD_CONNECTOR_REFRESH, + DeadConnectorRefreshJob.class, + () -> new DeadConnectorRefreshJob(dslContextFactory, connectorQueueFiller) ); } diff --git a/extensions/wrapper/clients/java-client/README.md b/extensions/wrapper/clients/java-client/README.md index 9eb0cd231..1c419272a 100644 --- a/extensions/wrapper/clients/java-client/README.md +++ b/extensions/wrapper/clients/java-client/README.md @@ -49,14 +49,14 @@ import de.sovity.edc.client.gen.model.KpiResult; */ public class WrapperClientExample { - public static final String CONNECTOR_ENDPOINT = "http://localhost:11002/api/management/v2"; - public static final String CONNECTOR_API_KEY = "..."; + public static final String MANAGEMENT_API_URL = "http://localhost:11002/api/management"; + public static final String MANAGEMENT_API_KEY = "..."; public static void main(String[] args) { // Configure Client EdcClient client = EdcClient.builder() - .managementApiUrl(CONNECTOR_ENDPOINT) - .managementApiKey(CONNECTOR_API_KEY) + .managementApiUrl(MANAGEMENT_API_URL) + .managementApiKey(MANAGEMENT_API_KEY) .build(); // EDC API Wrapper APIs are now available for use @@ -80,15 +80,15 @@ import de.sovity.edc.client.oauth2.SovityKeycloakUrl; */ public class WrapperClientExample { - public static final String CONNECTOR_ENDPOINT = - "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/data"; + public static final String MANAGEMENT_API_URL = + "https://{{your-connector}}.prod-sovity.azure.sovity.io/control/api/management"; public static final String CLIENT_ID = "{{your-connector}}-app"; public static final String CLIENT_SECRET = "..."; public static void main(String[] args) { // Configure Client EdcClient client = EdcClient.builder() - .managementApiUrl(CONNECTOR_ENDPOINT) + .managementApiUrl(MANAGEMENT_API_URL) .oauth2ClientCredentials(OAuth2ClientCredentials.builder() .tokenUrl(SovityKeycloakUrl.PRODUCTION) .clientId(CLIENT_ID) @@ -107,50 +107,66 @@ public class WrapperClientExample { Below are the examples of various tasks and the corresponding methods to be used from the Java-client. -| Task | Java-Client method | -|----------------------------------------------------------|-----------------------------------------------------------------------| -| Create Policy - uiAPI | `EdcClient.uiApi().createPolicyDefinition(policyDefinition)` | -| Create Policy - useCaseApi (allows AND/OR/XOR operators) | `EdcClient.useCaseApi().createPolicyDefinitionUseCase(createRequest)` | -| Create asset (Asset Creation after activate) | `EdcClient.uiApi().createAsset(uiAssetRequest)` | -| Create contract definition | `EdcClient.uiApi().createContractDefinition(contractDefinition)` | -| Create Offer on consumer dashboard (Catalog Browser) | `EdcClient.uiApi().getCatalogPageDataOffers(PROTOCOL_ENDPOINT)` | -| Accept contract (Contract Negotiation) | `EdcClient.uiApi().initiateContractNegotiation(negotiationRequest)` | -| Transfer Data (Initiate Transfer) | `EdcClient.uiApi().initiateTransfer(negotiation)` | +| Task | Java-Client method | +|------------------------------------------------------|---------------------------------------------------------------------| +| Create Policy | `EdcClient.uiApi().createPolicyDefinitionV2(policyDefinition)` | +| Create asset (Asset Creation after activate) | `EdcClient.uiApi().createAsset(uiAssetRequest)` | +| Create contract definition | `EdcClient.uiApi().createContractDefinition(contractDefinition)` | +| Create Offer on consumer dashboard (Catalog Browser) | `EdcClient.uiApi().getCatalogPageDataOffers(PROTOCOL_ENDPOINT)` | +| Accept contract (Contract Negotiation) | `EdcClient.uiApi().initiateContractNegotiation(negotiationRequest)` | +| Transfer Data (Initiate Transfer) | `EdcClient.uiApi().initiateTransfer(negotiation)` | These methods facilitate various operations such as creating policies, assets, contract definitions, browsing offers, accepting contracts, and initiating data transfers. -### Example Creating a Catena-Policy using operators (AND/OR/XOR) +### Example Creating a Catena-Policy using operators (AND/OR/XONE) The following example demonstrates how to create a Catena-Policy with linked conditions using the Java-client. ```java -var policyId = UUID.randomUUID().toString(); -var membershipElement = buildAtomicElement("Membership", OperatorDto.EQ, "active"); -var purposeElement = buildAtomicElement("PURPOSE", OperatorDto.EQ, "ID 3.1 Trace"); -var andElement = new Expression() - .expressionType(ExpressionTypeDto.AND) - .expressions(List.of(membershipElement, purposeElement)); -var permissionDto = new PermissionDto(andElement); -var createRequest = new PolicyCreateRequest(policyId, permissionDto); - -var response = client.useCaseApi().createPolicyDefinitionUseCase(createRequest); - -private Expression buildAtomicElement( +public String createCatenaXPolicy() { + var policyId = UUID.randomUUID().toString(); + + var expression = buildAnd( + buildConstraint("Membership", OperatorDto.EQ, "active"), + buildConstraint("PURPOSE", OperatorDto.EQ, "ID 3.1 Trace") + ); + + var policyCreateRequest = PolicyDefinitionCreateDto.builder() + .policyDefinitionId(policyId) + .expression(expression) + .build(); + + client.uiApi().createPolicyDefinition(policyCreateRequest); + + return policyId; +} + +private UiPolicyExpression buildAnd(UiPolicyExpression... expressions) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(Arrays.asList(expressions)) + .build(); +} + +private UiPolicyExpression buildConstraint( String left, OperatorDto operator, - String right) { - var atomicConstraint = new AtomicConstraintDto() - .leftExpression(left) - .operator(operator) - .rightExpression(right); - return new Expression() - .expressionType(ExpressionTypeDto.ATOMIC_CONSTRAINT) - .atomicConstraint(atomicConstraint); + String right +) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() + .left(left) + .operator(operator) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(right) + .build()) + .build()) + .build(); } ``` -The complete example can be seen in [this test](https://github.com/sovity/edc-ce/blob/main/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java). - ## License Apache License 2.0 - see [LICENSE](../../../../LICENSE) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index dd09224c1..81013da0c 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.ui; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; @@ -30,6 +29,8 @@ import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.TransferHistoryPage; import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; @@ -97,9 +98,18 @@ interface UiResource { @Path("pages/policy-page/policy-definitions") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Create a new Policy Definition") + @Deprecated + @Operation(description = "[Deprecated] Create a new Policy Definition from a list of constraints. " + + "Use createPolicyDefinitionV2 instead.", deprecated = true) IdResponseDto createPolicyDefinition(PolicyDefinitionCreateRequest policyDefinitionDtoDto); + @POST + @Path("v2/pages/policy-page/policy-definitions") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new Policy Definition") + IdResponseDto createPolicyDefinitionV2(PolicyDefinitionCreateDto policyDefinitionCreateDto); + @DELETE @Path("pages/policy-page/policy-definitions/{policyId}") @Produces(MediaType.APPLICATION_JSON) diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java similarity index 70% rename from extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java rename to extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java index 70392fff6..8bc4d076d 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionCreateRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java @@ -12,9 +12,10 @@ * */ -package de.sovity.edc.ext.wrapper.api.common.model; +package de.sovity.edc.ext.wrapper.api.ui.model; import com.fasterxml.jackson.annotation.JsonInclude; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -26,12 +27,12 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Data for creating a Policy Definition") -public class PolicyDefinitionCreateRequest { +@Schema(description = "Create a Policy Definition") +public class PolicyDefinitionCreateDto { @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) private String policyDefinitionId; - @Schema(description = "Policy Contents", requiredMode = Schema.RequiredMode.REQUIRED) - private UiPolicyCreateRequest policy; + @Schema(description = "Policy Expression", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicyExpression expression; } diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateRequest.java similarity index 62% rename from extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java rename to extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateRequest.java index da6b75996..911d90db6 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/model/PolicyCreateRequest.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateRequest.java @@ -12,10 +12,10 @@ * */ -package de.sovity.edc.ext.wrapper.api.usecase.model; +package de.sovity.edc.ext.wrapper.api.ui.model; import com.fasterxml.jackson.annotation.JsonInclude; -import de.sovity.edc.ext.wrapper.api.common.model.PermissionDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -27,13 +27,14 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Policy Creation Request Supporting Multiplicity Constraints.") -public class PolicyCreateRequest { - +@Deprecated +@Schema(description = "[Deprecated] Create a Policy Definition. Use PolicyDefinitionCreateDto", deprecated = true) +public class PolicyDefinitionCreateRequest { @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) private String policyDefinitionId; - @Schema(description = "Permission description for the policy to evaluate to TRUE.") - private PermissionDto permission; - + @Schema(description = "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)", + requiredMode = Schema.RequiredMode.REQUIRED, deprecated = true) + private UiPolicyCreateRequest policy; } + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionDto.java similarity index 90% rename from extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java rename to extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionDto.java index a0e917f2d..3c69aca84 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PolicyDefinitionDto.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionDto.java @@ -12,9 +12,10 @@ * */ -package de.sovity.edc.ext.wrapper.api.common.model; +package de.sovity.edc.ext.wrapper.api.ui.model; import com.fasterxml.jackson.annotation.JsonInclude; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java index 285e338b9..9b77b3c36 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionPage.java @@ -15,7 +15,6 @@ package de.sovity.edc.ext.wrapper.api.ui.model; import com.fasterxml.jackson.annotation.JsonInclude; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java index 51408dfce..b959ac0ec 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -14,11 +14,9 @@ package de.sovity.edc.ext.wrapper.api.usecase; -import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; -import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -61,11 +59,4 @@ List queryCatalog( @NotNull CatalogQuery catalogQuery ); - - @POST - @Path("policy-definition") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Create a new Policy Definition") - IdResponseDto createPolicyDefinitionUseCase(PolicyCreateRequest policyCreateRequest); } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java deleted file mode 100644 index 32b28eafb..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AssetDto.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.wrapper.api.common.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.RequiredArgsConstructor; - -import java.time.OffsetDateTime; -import java.util.Map; - -@Data -@AllArgsConstructor -@RequiredArgsConstructor -@Builder(toBuilder = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Asset Details") -public class AssetDto { - @Schema(description = "ID of asset", requiredMode = Schema.RequiredMode.REQUIRED) - private String assetId; - - @Schema(description = "Creation Date of asset", requiredMode = Schema.RequiredMode.REQUIRED) - private OffsetDateTime createdAt; - - @Schema(description = "Asset properties", requiredMode = Schema.RequiredMode.REQUIRED) - private Map properties; -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java deleted file mode 100644 index 8419b712a..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/AtomicConstraintDto.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - * - */ - -package de.sovity.edc.ext.wrapper.api.common.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.RequiredArgsConstructor; - - -@Data -@AllArgsConstructor -@RequiredArgsConstructor -@Builder(toBuilder = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = - "Type-Safe OpenAPI generator friendly Constraint DTO that supports an opinionated" - + " subset of the original EDC Constraint Entity.") -public class AtomicConstraintDto { - - @Schema(description = "Left part of the constraint.", - requiredMode = Schema.RequiredMode.REQUIRED) - private String leftExpression; - @Schema(description = "Operator to connect both parts of the constraint.", - requiredMode = Schema.RequiredMode.REQUIRED) - private OperatorDto operator; - @Schema(description = "Right part of the constraint.", - requiredMode = Schema.RequiredMode.REQUIRED) - private String rightExpression; -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java deleted file mode 100644 index 9fc30e0de..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/Expression.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.wrapper.api.common.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.RequiredArgsConstructor; - -import java.util.List; - -@Data -@AllArgsConstructor -@RequiredArgsConstructor -@Builder(toBuilder = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = - "Represents a single atomic constraint or a multiplicity constraint. The atomicConstraint" + - " will be evaluated if the constraintType is ATOMIC_CONSTRAINT.") -public class Expression { - - @Schema(description = "Either ATOMIC_CONSTRAINT or one of the multiplicity constraint types.") - private ExpressionType expressionType; - - @Schema(description = - "List of policy elements that are evaluated according the expressionType.") - private List expressions; - - @Schema(description = - "A single atomic constraint. Will be evaluated if the expressionType is set to " + - "ATOMIC_CONSTRAINT.") - private AtomicConstraintDto atomicConstraint; -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java deleted file mode 100644 index 33e21f83f..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/ExpressionType.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.sovity.edc.ext.wrapper.api.common.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -/** - * Sum type enum. - */ -@Schema(description = """ - Expression types: - * `ATOMIC_CONSTRAINT` - A single constraint for the policy - * `AND` - Several constraints, all of which must be respected - * `OR` - Several constraints, of which at least one must be respected - * `XOR` - Several constraints, of which exactly one must be respected - """, enumAsRef = true) -public enum ExpressionType { - ATOMIC_CONSTRAINT, AND, OR, XOR -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java index 0e31e0ff7..7eda3a545 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/OperatorDto.java @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - init + * Fraunhofer ISST - initial implementation + * sovity GmbH - documentation changes * */ @@ -17,11 +18,9 @@ import io.swagger.v3.oas.annotations.media.Schema; /** - * Equivalent of ODRL Policy Operator for our API Wrapper API. - * * @author tim.dahlmanns@isst.fraunhofer.de */ -@Schema(description = "Operator for policies", enumAsRef = true) +@Schema(description = "Type-Safe ODRL Policy Operator as supported by the sovity product landscape", enumAsRef = true) public enum OperatorDto { EQ, NEQ, diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java deleted file mode 100644 index a783f1348..000000000 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/PermissionDto.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - * - */ - -package de.sovity.edc.ext.wrapper.api.common.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.RequiredArgsConstructor; - - -@Data -@AllArgsConstructor -@RequiredArgsConstructor -@Builder(toBuilder = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -public class PermissionDto { - - @Schema(description = "Possible constraints for the permission", - requiredMode = RequiredMode.REQUIRED) - private Expression expression; -} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java index bf28fc1a9..0ed4590b9 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java @@ -29,7 +29,7 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Type-Safe Asset Metadata as needed by our UI") +@Schema(description = "Type-safe data offer metadata as supported by the sovity product landscape. Contains extension points.") public class UiAsset { @Schema(description = "'Live' vs 'On Request'", requiredMode = Schema.RequiredMode.REQUIRED) private DataSourceAvailability dataSourceAvailability; @@ -170,22 +170,22 @@ public class UiAsset { private String assetJsonLd; @Schema(description = "Contains serialized custom properties in the JSON format.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String customJsonAsString; @Schema(description = "Contains serialized custom properties in the JSON LD format. " + - "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + - "and will be affected by JSON LD compaction and expansion. " + - "Due to a technical limitation, the properties can't be booleans.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String customJsonLdAsString; @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String privateCustomJsonAsString; @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + - "The same limitations apply.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String privateCustomJsonLdAsString; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java index c49b7af1f..8ee241616 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java @@ -29,7 +29,7 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Type-Safe OpenAPI generator friendly Asset Create DTO that supports an opinionated subset of the original EDC Asset Entity.") +@Schema(description = "Type-safe data offer metadata for creating an asset as supported by the sovity product landscape. Contains extension points.") public class UiAssetCreateRequest { @Schema(description = "Data Source", requiredMode = Schema.RequiredMode.REQUIRED) private UiDataSource dataSource; diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java index 87257c694..31fd15d74 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditRequest.java @@ -29,7 +29,7 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Data for editing an asset.") +@Schema(description = "Type-safe data offer metadata for editing an asset as supported by the sovity product landscape. Contains extension points.") public class UiAssetEditRequest { @Schema(description = "Data Source", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private UiDataSource dataSourceOverrideOrNull; @@ -107,22 +107,22 @@ public class UiAssetEditRequest { private LocalDate temporalCoverageToInclusive; @Schema(description = "Contains serialized custom properties in the JSON format.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String customJsonAsString; @Schema(description = "Contains serialized custom properties in the JSON LD format. " + - "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + - "and will be affected by JSON LD compaction and expansion. " + - "Due to a technical limitation, the properties can't be booleans.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String customJsonLdAsString; @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String privateCustomJsonAsString; @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + - "The same limitations apply.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String privateCustomJsonLdAsString; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java index bb57db00c..8feef9ae8 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSource.java @@ -28,7 +28,7 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Data Offer Data Source Model. Supports certain Data Address types but also leaves a backdoor for custom Data Address Properties.") +@Schema(description = "Type-safe data source as supported by the sovity product landscape. Contains extension points for using custom data address properties.") public class UiDataSource { @Schema( description = "Data Address Type.", diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java index 381dcdb73..c995248d5 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpData.java @@ -28,6 +28,7 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "HTTP_DATA type Data Source.") public class UiDataSourceHttpData { @Schema( description = "HTTP Request Method", diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java index 0244a47ab..013848562 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiDataSourceHttpDataMethod.java @@ -11,7 +11,7 @@ * sovity GmbH - initial API and implementation * */ - + package de.sovity.edc.ext.wrapper.api.common.model; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java index 4118c7434..2b6317edc 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicy.java @@ -29,17 +29,17 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Type-Safe OpenAPI generator friendly Policy DTO as needed by our UI") +@Schema(description = "Type-Safe OpenAPI generator friendly ODLR policy subset as endorsed by sovity.") public class UiPolicy { @Schema(description = "EDC Policy JSON-LD. This is required because the EDC requires the " + - "full policy when initiating contract negotiations.", requiredMode = RequiredMode.REQUIRED) + "full policy when initiating contract negotiations.", requiredMode = RequiredMode.REQUIRED) private String policyJsonLd; - @Schema(description = "Conjunction of required expressions for the policy to evaluate to TRUE.") - private List constraints; + @Schema(description = "Policy expression") + private UiPolicyExpression expression; @Schema(description = "When trying to reduce the policy JSON-LD to our opinionated subset of functionalities, " + - "many fields and functionalities are unsupported. Should any discrepancies occur during " + - "the mapping process, we'll collect them here.", requiredMode = RequiredMode.REQUIRED) + "many fields and functionalities are unsupported. Should any discrepancies occur during " + + "the mapping process, we'll collect them here.", requiredMode = RequiredMode.REQUIRED) private List errors; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java index 1df6cd1dc..58fc11f22 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyConstraint.java @@ -27,7 +27,7 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "ODRL AtomicConstraint as supported by our UI") +@Schema(description = "ODRL AtomicConstraint as supported by the sovity product landscape. For example 'a EQ b', 'c IN [d, e, f]'") public class UiPolicyConstraint { @Schema(description = "Left side of the expression.", requiredMode = RequiredMode.REQUIRED) private String left; diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java index b1f18465a..e27806608 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyCreateRequest.java @@ -28,9 +28,10 @@ @RequiredArgsConstructor @Builder(toBuilder = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "Type-Safe OpenAPI generator friendly Policy Create DTO that supports an opinionated" - + " subset of the original EDC Policy Entity.") +@Deprecated +@Schema(description = "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)", + deprecated = true) public class UiPolicyCreateRequest { - @Schema(description = "Conjunction of required expressions for the policy to evaluate to TRUE.") + @Schema(description = "Conjunction of required constraints", deprecated = true) private List constraints; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpression.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpression.java new file mode 100644 index 000000000..4d30e337d --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpression.java @@ -0,0 +1,66 @@ +package de.sovity.edc.ext.wrapper.api.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.List; + + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "ODRL constraint as supported by the sovity product landscape") +public class UiPolicyExpression { + + @Schema(description = "Expression type", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicyExpressionType type; + + @Schema(description = "Only for types AND, OR, XONE. List of sub-expressions " + + "to be evaluated according to the expressionType.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private List expressions; + + @Schema(description = "Only for type CONSTRAINT. A single constraint.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private UiPolicyConstraint constraint; + + public static UiPolicyExpression empty() { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.EMPTY) + .build(); + } + + public static UiPolicyExpression constraint(UiPolicyConstraint constraint) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(constraint) + .build(); + } + + public static UiPolicyExpression and(List expressions) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(expressions) + .build(); + } + + public static UiPolicyExpression or(List expressions) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.OR) + .expressions(expressions) + .build(); + } + + public static UiPolicyExpression xone(List expressions) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.XONE) + .expressions(expressions) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpressionType.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpressionType.java new file mode 100644 index 000000000..2267a4abc --- /dev/null +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyExpressionType.java @@ -0,0 +1,20 @@ +package de.sovity.edc.ext.wrapper.api.common.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = """ + Ui Policy Expression types: + * `CONSTRAINT` - Expression 'a=b' + * `AND` - Conjunction of several expressions. Evaluates to true iff all child expressions are true. + * `OR` - Disjunction of several expressions. Evaluates to true iff at least one child expression is true. + * `XONE` - XONE operation. Evaluates to true iff exactly one child expression is true. + """, enumAsRef = true) +public enum UiPolicyExpressionType { + EMPTY, + CONSTRAINT, + AND, + OR, + XONE +} + + diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java index 7cb251703..08176e9ae 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiPolicyLiteral.java @@ -45,22 +45,22 @@ public class UiPolicyLiteral { public static UiPolicyLiteral ofString(String string) { return UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(string) - .build(); + .type(UiPolicyLiteralType.STRING) + .value(string) + .build(); } public static UiPolicyLiteral ofJson(String jsonString) { return UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.JSON) - .value(jsonString) - .build(); + .type(UiPolicyLiteralType.JSON) + .value(jsonString) + .build(); } public static UiPolicyLiteral ofStringList(Collection strings) { return UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING_LIST) - .valueList(new ArrayList<>(strings)) - .build(); + .type(UiPolicyLiteralType.STRING_LIST) + .valueList(new ArrayList<>(strings)) + .build(); } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java index 03c45f5af..4ac252075 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapper.java @@ -11,7 +11,7 @@ * sovity GmbH - initial API and implementation * */ - + package de.sovity.edc.ext.wrapper.api.common.mappers; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapper.java new file mode 100644 index 000000000..22d95bf34 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class LegacyPolicyMapper { + /** + * Builds a {@link UiPolicyExpression} from the legacy {@link UiPolicyCreateRequest}. + * + * @param createRequest {@link UiPolicyCreateRequest} + * @return {@link UiPolicyExpression} + */ + @Deprecated + public UiPolicyExpression buildUiPolicyExpression(UiPolicyCreateRequest createRequest) { + if (createRequest == null) { + return UiPolicyExpression.empty(); + } + + return buildUiPolicyExpression(createRequest.getConstraints()); + } + + private UiPolicyExpression buildUiPolicyExpression(List expressions) { + UiPolicyExpression expression; + if (expressions == null || expressions.isEmpty()) { + expression = UiPolicyExpression.empty(); + } else if (expressions.size() == 1) { + expression = UiPolicyExpression.constraint(expressions.get(0)); + } else { + expression = UiPolicyExpression.and( + expressions.stream().map(UiPolicyExpression::constraint).toList() + ); + } + return expression; + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java index 168dc5666..3b69efa56 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapper.java @@ -15,37 +15,27 @@ package de.sovity.edc.ext.wrapper.api.common.mappers; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.utils.FailedMappingException; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; -import de.sovity.edc.ext.wrapper.api.common.model.Expression; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicy; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; import de.sovity.edc.utils.JsonUtils; -import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.JsonObject; import lombok.RequiredArgsConstructor; import org.eclipse.edc.policy.model.Action; -import org.eclipse.edc.policy.model.AndConstraint; -import org.eclipse.edc.policy.model.Constraint; -import org.eclipse.edc.policy.model.OrConstraint; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.policy.model.PolicyType; -import org.eclipse.edc.policy.model.XoneConstraint; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; import static de.sovity.edc.utils.JsonUtils.toJson; @RequiredArgsConstructor public class PolicyMapper { - private final ConstraintExtractor constraintExtractor; - private final AtomicConstraintMapper atomicConstraintMapper; + private final ExpressionExtractor expressionExtractor; + private final ExpressionMapper expressionMapper; private final TypeTransformerRegistry typeTransformerRegistry; /** @@ -59,13 +49,13 @@ public class PolicyMapper { public UiPolicy buildUiPolicy(Policy policy) { MappingErrors errors = MappingErrors.root(); - var constraints = constraintExtractor.getPermissionConstraints(policy, errors); + var expression = expressionExtractor.getPermissionExpression(policy, errors); return UiPolicy.builder() - .policyJsonLd(toJson(buildPolicyJsonLd(policy))) - .constraints(constraints) - .errors(errors.getErrors()) - .build(); + .policyJsonLd(toJson(buildPolicyJsonLd(policy))) + .expression(expression) + .errors(errors.getErrors()) + .build(); } /** @@ -73,62 +63,23 @@ public UiPolicy buildUiPolicy(Policy policy) { *

      * This operation is lossless. * - * @param policyCreateDto policy + * @param expression policy * @return ODRL policy */ - public Policy buildPolicy(UiPolicyCreateRequest policyCreateDto) { - var constraints = new ArrayList(atomicConstraintMapper.buildAtomicConstraints( - policyCreateDto.getConstraints())); + public Policy buildPolicy(UiPolicyExpression expression) { + var constraints = expressionMapper.buildConstraint(expression); var action = Action.Builder.newInstance().type(PolicyValidator.ALLOWED_ACTION).build(); var permission = Permission.Builder.newInstance() - .action(action) - .constraints(constraints) - .build(); - - return Policy.Builder.newInstance() - .type(PolicyType.SET) - .permission(permission) - .build(); - } - - public Policy buildPolicy(List constraintElements) { - var constraints = buildConstraints(constraintElements); - var action = Action.Builder.newInstance().type(Prop.Odrl.USE).build(); - var permission = Permission.Builder.newInstance() - .action(action) - .constraints(constraints) - .build(); + .action(action) + .constraints(constraints.stream().toList()) + .build(); return Policy.Builder.newInstance() - .type(PolicyType.SET) - .permission(permission) - .build(); - } - - @NotNull - private List buildConstraints(List expressions) { - return expressions.stream() - .map(this::buildConstraint) - .toList(); - } - - private Constraint buildConstraint(Expression expression) { - var subExpressions = expression.getExpressions(); - return switch (expression.getExpressionType()) { - case ATOMIC_CONSTRAINT -> - atomicConstraintMapper.buildAtomicConstraint(expression.getAtomicConstraint()); - case AND -> AndConstraint.Builder.newInstance() - .constraints(buildConstraints(subExpressions)) - .build(); - case OR -> OrConstraint.Builder.newInstance() - .constraints(buildConstraints(subExpressions)) - .build(); - case XOR -> XoneConstraint.Builder.newInstance() - .constraints(buildConstraints(subExpressions)) - .build(); - }; + .type(PolicyType.SET) + .permission(permission) + .build(); } /** @@ -141,7 +92,7 @@ private Constraint buildConstraint(Expression expression) { */ public Policy buildPolicy(JsonObject policyJsonLd) { return typeTransformerRegistry.transform(policyJsonLd, Policy.class) - .orElseThrow(FailedMappingException::ofFailure); + .orElseThrow(FailedMappingException::ofFailure); } /** @@ -166,6 +117,6 @@ public Policy buildPolicy(String policyJsonLd) { */ public JsonObject buildPolicyJsonLd(Policy policy) { return typeTransformerRegistry.transform(policy, JsonObject.class) - .orElseThrow(FailedMappingException::ofFailure); + .orElseThrow(FailedMappingException::ofFailure); } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java index a182b75a0..7e22d9f59 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/utils/JsonBuilderUtils.java @@ -52,8 +52,8 @@ public static void addNonNull(JsonObjectBuilder builder, String key, LocalDate v * Adds non-null non-blank trimmed items as a JSON Array * * @param builder target object - * @param key key - * @param values list of values + * @param key key + * @param values list of values */ public static void addNotBlankStringArray(JsonObjectBuilder builder, String key, List values) { var filteredItems = (values == null ? Stream.of() : values.stream()) diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java index b679687df..cde71667d 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpHeaderMapper.java @@ -11,7 +11,7 @@ * sovity GmbH - initial API and implementation * */ - + package de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http; import de.sovity.edc.utils.jsonld.vocab.Prop; diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java index 7586a2081..2a4c45a2b 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapper.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.policy; -import de.sovity.edc.ext.wrapper.api.common.model.AtomicConstraintDto; import de.sovity.edc.ext.wrapper.api.common.model.OperatorDto; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import lombok.NonNull; @@ -22,7 +21,6 @@ import org.eclipse.edc.policy.model.AtomicConstraint; import org.eclipse.edc.policy.model.LiteralExpression; -import java.util.List; import java.util.Optional; @RequiredArgsConstructor @@ -31,53 +29,56 @@ public class AtomicConstraintMapper { private final OperatorMapper operatorMapper; /** - * Create ODRL {@link AtomicConstraint}s from {@link UiPolicyConstraint}s + * Create ODRL {@link AtomicConstraint} from {@link UiPolicyConstraint} *

      * This operation is lossless. * - * @param constraints ui constraints - * @return ODRL constraints + * @param constraint ui constraint + * @return ODRL constraint */ - public List buildAtomicConstraints(List constraints) { - if (constraints == null) { - return List.of(); - } + public AtomicConstraint buildAtomicConstraint(UiPolicyConstraint constraint) { + var left = constraint.getLeft(); + var operator = operatorMapper.getOperator(constraint.getOperator()); + var right = literalMapper.getUiLiteralValue(constraint.getRight()); - return constraints.stream() - .map(this::buildAtomicConstraint) - .toList(); + return AtomicConstraint.Builder.newInstance() + .leftExpression(new LiteralExpression(left)) + .operator(operator) + .rightExpression(new LiteralExpression(right)) + .build(); } + /** * Create {@link UiPolicyConstraint} from ODRL {@link AtomicConstraint} *

      * This operation is lossy. * * @param atomicConstraint atomic contraints - * @param errors errors + * @param errors errors * @return ui policy constraint */ public Optional buildUiConstraint( - @NonNull AtomicConstraint atomicConstraint, - MappingErrors errors + @NonNull AtomicConstraint atomicConstraint, + MappingErrors errors ) { var leftValue = literalMapper.getExpressionString(atomicConstraint.getLeftExpression(), - errors.forChildObject("leftExpression")); + errors.forChildObject("leftExpression")); var operator = getOperator(atomicConstraint, errors); var rightValue = literalMapper.getExpressionValue(atomicConstraint.getRightExpression(), - errors.forChildObject("rightExpression")); + errors.forChildObject("rightExpression")); if (leftValue.isEmpty() || rightValue.isEmpty() || operator.isEmpty()) { return Optional.empty(); } UiPolicyConstraint result = UiPolicyConstraint.builder() - .left(leftValue.get()) - .operator(operator.get()) - .right(rightValue.get()) - .build(); + .left(leftValue.get()) + .operator(operator.get()) + .right(rightValue.get()) + .build(); return Optional.of(result); } @@ -92,28 +93,4 @@ private Optional getOperator(AtomicConstraint atomicConstraint, Map return Optional.of(operatorMapper.getOperatorDto(operator)); } - - private AtomicConstraint buildAtomicConstraint(UiPolicyConstraint constraint) { - var left = constraint.getLeft(); - var operator = operatorMapper.getOperator(constraint.getOperator()); - var right = literalMapper.getUiLiteralValue(constraint.getRight()); - - return AtomicConstraint.Builder.newInstance() - .leftExpression(new LiteralExpression(left)) - .operator(operator) - .rightExpression(new LiteralExpression(right)) - .build(); - } - - public AtomicConstraint buildAtomicConstraint(AtomicConstraintDto atomicConstraint) { - var left = atomicConstraint.getLeftExpression(); - var operator = operatorMapper.getOperator(atomicConstraint.getOperator()); - var right = atomicConstraint.getRightExpression(); - - return AtomicConstraint.Builder.newInstance() - .leftExpression(new LiteralExpression(left)) - .operator(operator) - .rightExpression(new LiteralExpression(right)) - .build(); - } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java deleted file mode 100644 index 405d166dc..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractor.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2023 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - init - * - */ - -package de.sovity.edc.ext.wrapper.api.common.mappers.policy; - -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; -import lombok.RequiredArgsConstructor; -import org.eclipse.edc.policy.model.AndConstraint; -import org.eclipse.edc.policy.model.AtomicConstraint; -import org.eclipse.edc.policy.model.Constraint; -import org.eclipse.edc.policy.model.OrConstraint; -import org.eclipse.edc.policy.model.Permission; -import org.eclipse.edc.policy.model.Policy; -import org.eclipse.edc.policy.model.XoneConstraint; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@RequiredArgsConstructor -public class ConstraintExtractor { - private final PolicyValidator policyValidator; - private final AtomicConstraintMapper atomicConstraintMapper; - - /** - * Build {@link UiPolicyConstraint}s from an ODRL {@link Policy}. - *

      - * This operation is lossy which is why we document warnings / errors in {@link MappingErrors}. - * - * @param policy ODRL policy - * @param errors mapping errors - * @return ui policy constraints - */ - public List getPermissionConstraints(Policy policy, MappingErrors errors) { - policyValidator.validateOtherPolicyFieldsUnset(policy, errors); - - var permissions = policy.getPermissions(); - if (permissions == null) { - return List.of(); - } - - - List constraints = new ArrayList<>(); - for (int iPermission = 0; iPermission < permissions.size(); iPermission++) { - var permissionErrors = errors.forChildObject("permissions").forChildArrayElement(iPermission); - var permission = permissions.get(iPermission); - constraints.addAll(getPermissionConstraints(permission, permissionErrors)); - } - return constraints; - } - - private List getPermissionConstraints(Permission permission, MappingErrors errors) { - policyValidator.validateOtherPermissionFieldsUnset(permission, errors); - - if (permission == null) { - return List.of(); - } - - var constraints = permission.getConstraints(); - if (constraints == null) { - return List.of(); - } - - var constraintsMapped = new ArrayList(); - for (int i = 0; i < constraints.size(); i++) { - var constraintErrors = errors.forChildObject("constraints").forChildArrayElement(i); - var constraint = constraints.get(i); - - var constraintMapped = buildConstraint(constraint, constraintErrors); - constraintMapped.ifPresent(constraintsMapped::add); - } - return constraintsMapped; - } - - private Optional buildConstraint(Constraint constraint, MappingErrors errors) { - if (constraint == null) { - errors.add("Constraint is null."); - return Optional.empty(); - } - - if (constraint instanceof XoneConstraint) { - errors.add("XoneConstraints are currently unsupported."); - return Optional.empty(); - } - - if (constraint instanceof AndConstraint) { - errors.add("AndConstraints are currently unsupported."); - return Optional.empty(); - } - - if (constraint instanceof OrConstraint) { - errors.add("OrConstraints are currently unsupported."); - return Optional.empty(); - } - - if (!(constraint instanceof AtomicConstraint)) { - errors.add("Unknown constraint type %s.".formatted(constraint.getClass().getName())); - return Optional.empty(); - } - - return atomicConstraintMapper.buildUiConstraint((AtomicConstraint) constraint, errors); - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractor.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractor.java new file mode 100644 index 000000000..496659e11 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractor.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class ExpressionExtractor { + private final PolicyValidator policyValidator; + private final ExpressionMapper expressionMapper; + + /** + * Build {@link UiPolicyExpression} from an ODRL {@link Policy}. + *

      + * This operation is lossy which is why we document warnings / errors in {@link MappingErrors}. + * + * @param policy ODRL policy + * @param errors mapping errors + * @return ui policy expression + */ + public UiPolicyExpression getPermissionExpression(Policy policy, MappingErrors errors) { + var expressions = getPermissionExpressions(policy, errors); + if (expressions.isEmpty()) { + return UiPolicyExpression.empty(); + } else if (expressions.size() == 1) { + return expressions.get(0); + } else { + return UiPolicyExpression.and(expressions); + } + } + + /** + * Build {@link UiPolicyExpression}s from an ODRL {@link Policy}. + *

      + * This operation is lossy which is why we document warnings / errors in {@link MappingErrors}. + * + * @param policy ODRL policy + * @param errors mapping errors + * @return ui policy expressions + */ + private List getPermissionExpressions(Policy policy, MappingErrors errors) { + policyValidator.validateOtherPolicyFieldsUnset(policy, errors); + + var permissions = policy.getPermissions(); + if (permissions == null) { + return List.of(); + } + + if (permissions.size() > 1) { + errors.add("Multiple permissions were present. Prefer using a conjunction using AND."); + } + + List expressions = new ArrayList<>(); + for (int iPermission = 0; iPermission < permissions.size(); iPermission++) { + var permissionErrors = errors.forChildObject("permissions").forChildArrayElement(iPermission); + var permission = permissions.get(iPermission); + expressions.addAll(getPermissionExpressions(permission, permissionErrors)); + } + return expressions; + } + + private List getPermissionExpressions(Permission permission, MappingErrors errors) { + policyValidator.validateOtherPermissionFieldsUnset(permission, errors); + + if (permission == null) { + return List.of(); + } + + var constraints = permission.getConstraints(); + if (constraints != null && constraints.size() > 1) { + errors.forChildObject("constraints") + .add("Multiple constraints were present. Prefer using a conjunction using AND."); + } + + return expressionMapper.buildUiPolicyExpressions( + constraints, + errors.forChildObject("constraints") + ); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapper.java new file mode 100644 index 000000000..025ce1488 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapper.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - init + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpressionType; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.eclipse.edc.policy.model.AndConstraint; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.OrConstraint; +import org.eclipse.edc.policy.model.XoneConstraint; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class ExpressionMapper { + private final AtomicConstraintMapper atomicConstraintMapper; + + public List buildConstraints( + @Nullable List expressions + ) { + if (expressions == null) { + return List.of(); + } + + return expressions.stream() + .map(this::buildConstraint) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + @NotNull + public List buildUiPolicyExpressions( + @Nullable List constraints, + @NonNull MappingErrors errors + ) { + if (constraints == null) { + errors.add("Constraints are null."); + return List.of(); + } + + var expressions = new ArrayList(); + for (int i = 0; i < constraints.size(); i++) { + var constraintErrors = errors.forChildArrayElement(i); + var constraint = constraints.get(i); + + buildUiPolicyExpression(constraint, constraintErrors).ifPresent(expressions::add); + } + return expressions; + } + + public Optional buildConstraint(UiPolicyExpression expression) { + if (expression == null || expression.getType() == null) { + return Optional.empty(); + } + + return switch (expression.getType()) { + case EMPTY -> Optional.empty(); + case AND -> Optional.of(AndConstraint.Builder.newInstance() + .constraints(buildConstraints(expression.getExpressions())) + .build()); + case OR -> Optional.of(OrConstraint.Builder.newInstance() + .constraints(buildConstraints(expression.getExpressions())) + .build()); + case XONE -> Optional.of(XoneConstraint.Builder.newInstance() + .constraints(buildConstraints(expression.getExpressions())) + .build()); + case CONSTRAINT -> Optional.of(atomicConstraintMapper + .buildAtomicConstraint(expression.getConstraint())); + }; + } + + private Optional buildUiPolicyExpression(Constraint constraint, MappingErrors errors) { + if (constraint == null) { + errors.add("Expression is null."); + return Optional.empty(); + } + + if (constraint instanceof XoneConstraint xone) { + return buildMultiUiPolicyExpression( + UiPolicyExpressionType.XONE, + xone.getConstraints(), + errors.forChildObject("constraints") + ); + } else if (constraint instanceof AndConstraint and) { + return buildMultiUiPolicyExpression( + UiPolicyExpressionType.AND, + and.getConstraints(), + errors.forChildObject("constraints") + ); + } else if (constraint instanceof OrConstraint or) { + return buildMultiUiPolicyExpression( + UiPolicyExpressionType.OR, + or.getConstraints(), + errors.forChildObject("constraints") + ); + } else if (constraint instanceof AtomicConstraint atomic) { + return atomicConstraintMapper.buildUiConstraint(atomic, errors) + .map(this::buildConstraintUiPolicyExpression); + } + + errors.add("Unknown expression type %s.".formatted(constraint.getClass().getName())); + return Optional.empty(); + } + + private Optional buildMultiUiPolicyExpression( + UiPolicyExpressionType type, + List constraints, + MappingErrors errors + ) { + var expressions = buildUiPolicyExpressions(constraints, errors); + var expression = UiPolicyExpression.builder() + .type(type) + .expressions(expressions) + .build(); + return Optional.of(expression); + } + + private UiPolicyExpression buildConstraintUiPolicyExpression(UiPolicyConstraint constraint) { + return UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(constraint) + .build(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java index f271cd773..a733d13a7 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/LiteralMapper.java @@ -42,23 +42,23 @@ public Object getUiLiteralValue(UiPolicyLiteral literal) { } public Optional getExpressionString( - Expression expression, - MappingErrors errors + Expression expression, + MappingErrors errors ) { return getLiteralExpression(expression, errors).flatMap(literalExpression -> - getLiteralExpressionString(literalExpression, errors)); + getLiteralExpressionString(literalExpression, errors)); } public Optional getExpressionValue( - Expression expression, - MappingErrors errors + Expression expression, + MappingErrors errors ) { return getLiteralExpression(expression, errors).flatMap(this::getLiteralExpressionValue); } private Optional getLiteralExpressionString( - LiteralExpression literalExpression, - MappingErrors errors + LiteralExpression literalExpression, + MappingErrors errors ) { var value = literalExpression.getValue(); if (value == null) { @@ -84,7 +84,7 @@ private Optional getLiteralExpressionValue(LiteralExpression li } boolean isStringList = value instanceof Collection && ((Collection) value).stream() - .allMatch(it -> it == null || it instanceof String); + .allMatch(it -> it == null || it instanceof String); if (isStringList) { return Optional.of(UiPolicyLiteral.ofStringList((Collection) value)); } diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapperTest.java new file mode 100644 index 000000000..43f5a883a --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/LegacyPolicyMapperTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@SuppressWarnings("deprecated") +@ExtendWith(MockitoExtension.class) +public class LegacyPolicyMapperTest { + @InjectMocks + LegacyPolicyMapper legacyPolicyMapper; + + @Test + void buildUiPolicyExpression_null() { + // arrange + UiPolicyCreateRequest request = null; + + // act + UiPolicyExpression result = legacyPolicyMapper.buildUiPolicyExpression(request); + + // assert + assertThat(result).isEqualTo(UiPolicyExpression.empty()); + } + + @Test + void buildUiPolicyExpression_expressionsNull() { + // arrange + var request = new UiPolicyCreateRequest(); + request.setConstraints(null); + + // act + UiPolicyExpression result = legacyPolicyMapper.buildUiPolicyExpression(request); + + // assert + assertThat(result).isEqualTo(UiPolicyExpression.empty()); + } + + @Test + void buildUiPolicyExpression_emptyExpressions() { + // arrange + var request = new UiPolicyCreateRequest(); + request.setConstraints(List.of()); + + // act + UiPolicyExpression result = legacyPolicyMapper.buildUiPolicyExpression(request); + + // assert + assertThat(result).isEqualTo(UiPolicyExpression.empty()); + } + + @Test + void buildUiPolicyExpression_singleExpression() { + // arrange + var request = new UiPolicyCreateRequest(); + var expression = new UiPolicyConstraint(); + request.setConstraints(List.of(expression)); + + // act + UiPolicyExpression result = legacyPolicyMapper.buildUiPolicyExpression(request); + + // assert + assertThat(result).isEqualTo(UiPolicyExpression.constraint(expression)); + } + + @Test + void buildUiPolicyExpression_multipleExpressions() { + // arrange + var request = new UiPolicyCreateRequest(); + var constraint1 = mock(UiPolicyConstraint.class); + var constraint2 = mock(UiPolicyConstraint.class); + request.setConstraints(List.of(constraint1, constraint2)); + + // act + UiPolicyExpression result = legacyPolicyMapper.buildUiPolicyExpression(request); + + // assert + assertThat(result).isEqualTo(UiPolicyExpression.and(List.of( + UiPolicyExpression.constraint(constraint1), + UiPolicyExpression.constraint(constraint2) + ))); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java index 2d01fe7e7..7ef734cfe 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java @@ -14,127 +14,106 @@ package de.sovity.edc.ext.wrapper.api.common.mappers; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; -import de.sovity.edc.ext.wrapper.api.common.model.AtomicConstraintDto; -import de.sovity.edc.ext.wrapper.api.common.model.Expression; -import de.sovity.edc.ext.wrapper.api.common.model.ExpressionType; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import jakarta.json.Json; import jakarta.json.JsonObject; -import lombok.SneakyThrows; +import org.eclipse.edc.policy.model.AndConstraint; import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.OrConstraint; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.policy.model.PolicyType; +import org.eclipse.edc.policy.model.XoneConstraint; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; +import java.util.Optional; -import static de.sovity.edc.ext.wrapper.api.common.model.ExpressionType.ATOMIC_CONSTRAINT; -import static de.sovity.edc.utils.JsonUtils.parseJsonObj; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class PolicyMapperTest { @InjectMocks PolicyMapper policyMapper; - @Mock - TypeTransformerRegistry transformerRegistry; - + ExpressionExtractor expressionExtractor; @Mock - ConstraintExtractor constraintExtractor; - + ExpressionMapper expressionMapper; @Mock - AtomicConstraintMapper atomicConstraintMapper; - + TypeTransformerRegistry typeTransformerRegistry; @Test - @SneakyThrows - void test_buildPolicyDto() { - try (MockedStatic mappingErrors = mockStatic(MappingErrors.class)) { - // arrange - var policy = mock(Policy.class); - var errors = mock(MappingErrors.class); - var constraints = List.of(mock(UiPolicyConstraint.class)); - - when(errors.getErrors()).thenReturn(List.of("error1")); - - mappingErrors.when(MappingErrors::root).thenReturn(errors); - when(constraintExtractor.getPermissionConstraints(policy, errors)).thenReturn(constraints); - when(transformerRegistry.transform(policy, JsonObject.class)).thenReturn(Result.success(parseJsonObj("{}"))); - - // act - var actual = policyMapper.buildUiPolicy(policy); - - // assert - assertThat(actual.getPolicyJsonLd()).isEqualTo("{}"); - assertThat(actual.getConstraints()).isEqualTo(constraints); - assertThat(actual.getErrors()).isEqualTo(List.of("error1")); - } + void buildUiPolicy() { + // arrange + var policy = mock(Policy.class); + var expression = mock(UiPolicyExpression.class); + + when(expressionExtractor.getPermissionExpression(eq(policy), any())).thenAnswer(i -> { + var errors = i.getArgument(1, MappingErrors.class); + errors.add("test"); + return expression; + }); + + when(typeTransformerRegistry.transform(eq(policy), eq(JsonObject.class))) + .thenReturn(Result.success(Json.createObjectBuilder().add("a", "b").build())); + + // act + var actual = policyMapper.buildUiPolicy(policy); + + // assert + assertThat(actual.getExpression()).isEqualTo(expression); + assertThat(actual.getErrors()).containsExactly("$: test"); + assertThat(actual.getPolicyJsonLd()).isEqualTo("{\"a\":\"b\"}"); } @Test - void test_buildPolicy() { + void buildPolicy_constraintExtracted() { // arrange - var constraint = mock(UiPolicyConstraint.class); - var createRequest = new UiPolicyCreateRequest(List.of(constraint)); - - var expected = mock(AtomicConstraint.class); - when(atomicConstraintMapper.buildAtomicConstraints(eq(List.of(constraint)))) - .thenReturn(List.of(expected)); + var uiExpression = mock(UiPolicyExpression.class); + var constraint = mock(Constraint.class); + when(expressionMapper.buildConstraint(uiExpression)) + .thenReturn(Optional.of(constraint)); // act - var actual = policyMapper.buildPolicy(createRequest); + var actual = policyMapper.buildPolicy(uiExpression); // assert assertThat(actual.getType()).isEqualTo(PolicyType.SET); assertThat(actual.getPermissions()).hasSize(1); - assertThat(actual.getPermissions().get(0).getConstraints()).hasSize(1); assertThat(actual.getPermissions().get(0).getAction().getType()).isEqualTo("USE"); - assertThat(actual.getPermissions().get(0).getConstraints().get(0)).isSameAs(expected); + assertThat(actual.getPermissions().get(0).getConstraints()).hasSize(1); + assertThat(actual.getPermissions().get(0).getConstraints()).containsExactly(constraint); } - @ParameterizedTest - @ValueSource(strings = {"AND", "OR", "XOR"}) - void buildGenericPolicy(String constraintTypeString) { + @Test + void buildPolicy_noConstraint() { // arrange - var expressionType = ExpressionType.valueOf(constraintTypeString); - var incomingConstraint = mock(AtomicConstraintDto.class); - var mappedAtomicConstraint = mock(AtomicConstraint.class); - var atomicConstraint = new Expression(ATOMIC_CONSTRAINT, List.of(), incomingConstraint); - var atomicConstraints = List.of(atomicConstraint, atomicConstraint); - var baseConstraintElement = new Expression(expressionType, atomicConstraints, null); + var uiExpression = mock(UiPolicyExpression.class); + when(expressionMapper.buildConstraint(uiExpression)) + .thenReturn(Optional.empty()); // act - when(atomicConstraintMapper - .buildAtomicConstraint(eq(incomingConstraint))) - .thenReturn(mappedAtomicConstraint); - var policy = policyMapper.buildPolicy(List.of(baseConstraintElement)); + var actual = policyMapper.buildPolicy(uiExpression); // assert - assertThat(policy.getType()).isEqualTo(PolicyType.SET); - assertThat(policy.getPermissions()).hasSize(1); - var permission = policy.getPermissions().get(0); - assertThat(permission.getConstraints()).hasSize(1); - assertThat(permission.getAction().getType()).isEqualTo("USE"); - - var constraintObject = permission.getConstraints().get(0); - assertNotNull(constraintObject); + assertThat(actual.getType()).isEqualTo(PolicyType.SET); + assertThat(actual.getPermissions()).hasSize(1); + assertThat(actual.getPermissions().get(0).getConstraints()).isEmpty(); + assertThat(actual.getPermissions().get(0).getAction().getType()).isEqualTo("USE"); } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java index 0a3b718f4..04eac28c7 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/AtomicConstraintMapperTest.java @@ -38,18 +38,6 @@ class AtomicConstraintMapperTest { - @Test - void test_buildAtomicConstraint_null() { - // arrange - var atomicConstraintMapper = newAtomicConstraintMapper(); - - // act - var actual = atomicConstraintMapper.buildAtomicConstraints(null); - - // assert - assertThat(actual).isEmpty(); - } - @Test void test_buildAtomicConstraint() { // arrange @@ -62,16 +50,14 @@ void test_buildAtomicConstraint() { when(literalMapper.getUiLiteralValue(right)).thenReturn("right"); // act - var actual = atomicConstraintMapper.buildAtomicConstraints(List.of(constraint)); + var actual = atomicConstraintMapper.buildAtomicConstraint(constraint); // assert - assertThat(actual).hasSize(1); - var atomicConstraint = actual.get(0); - assertThat(atomicConstraint.getLeftExpression()).isInstanceOfSatisfying(LiteralExpression.class, literalExpression -> + assertThat(actual.getLeftExpression()).isInstanceOfSatisfying(LiteralExpression.class, literalExpression -> assertThat(literalExpression.getValue()).isEqualTo("left")); - assertThat(atomicConstraint.getRightExpression()).isInstanceOfSatisfying(LiteralExpression.class, literalExpression -> + assertThat(actual.getRightExpression()).isInstanceOfSatisfying(LiteralExpression.class, literalExpression -> assertThat(literalExpression.getValue()).isEqualTo("right")); - assertThat(atomicConstraint.getOperator()).isEqualTo(Operator.EQ); + assertThat(actual.getOperator()).isEqualTo(Operator.EQ); } @Test diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java deleted file mode 100644 index 0245b6bb0..000000000 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ConstraintExtractorTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2022 sovity GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * sovity GmbH - initial API and implementation - * - */ - -package de.sovity.edc.ext.wrapper.api.common.mappers.policy; - -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; -import org.eclipse.edc.policy.model.AndConstraint; -import org.eclipse.edc.policy.model.AtomicConstraint; -import org.eclipse.edc.policy.model.OrConstraint; -import org.eclipse.edc.policy.model.Permission; -import org.eclipse.edc.policy.model.Policy; -import org.eclipse.edc.policy.model.XoneConstraint; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ConstraintExtractorTest { - @InjectMocks - ConstraintExtractor constraintExtractor; - - @Mock - PolicyValidator policyValidator; - - @Mock - AtomicConstraintMapper atomicConstraintMapper; - - @Test - void test_getPermissionConstraints_null() { - // arrange - var policy = Policy.Builder.newInstance().build(); - var errors = MappingErrors.root(); - - // act - var actual = constraintExtractor.getPermissionConstraints(policy, errors); - - // assert - assertThat(actual).isEmpty(); - verify(policyValidator).validateOtherPolicyFieldsUnset(policy, errors); - } - - @Test - void test_getPermissionConstraints_many_constraints() { - // arrange - var first = mock(AtomicConstraint.class); - var other = mock(AtomicConstraint.class); - var permission = Permission.Builder.newInstance() - .constraint(null) - .constraint(first) - .constraint(other) - .constraint(mock(AndConstraint.class)) - .constraint(mock(OrConstraint.class)) - .constraint(mock(XoneConstraint.class)) - .build(); - var policy = Policy.Builder.newInstance() - .permission(null) - .permission(permission) - .permission(Permission.Builder.newInstance().build()) - .build(); - var errors = MappingErrors.root(); - - var expected = mock(UiPolicyConstraint.class); - when(atomicConstraintMapper.buildUiConstraint(same(first), any())).thenReturn(Optional.of(expected)); - when(atomicConstraintMapper.buildUiConstraint(same(other), any())).thenReturn(Optional.empty()); - - // act - var actual = constraintExtractor.getPermissionConstraints(policy, errors); - - // assert - verify(policyValidator).validateOtherPermissionFieldsUnset(same(permission), any()); - verify(policyValidator).validateOtherPermissionFieldsUnset(eq(null), any()); - assertThat(actual).containsExactly(expected); - assertThat(errors.getErrors()).containsExactlyInAnyOrder( - "$.permissions[1].constraints[0]: Constraint is null.", - "$.permissions[1].constraints[3]: AndConstraints are currently unsupported.", - "$.permissions[1].constraints[4]: OrConstraints are currently unsupported.", - "$.permissions[1].constraints[5]: XoneConstraints are currently unsupported." - ); - } -} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractorTest.java new file mode 100644 index 000000000..b4e96b340 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionExtractorTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpressionType; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExpressionExtractorTest { + @InjectMocks + ExpressionExtractor expressionExtractor; + + @Mock + PolicyValidator policyValidator; + + @Mock + ExpressionMapper expressionMapper; + + @Test + void test_getPermissionConstraints_null() { + // arrange + var policy = Policy.Builder.newInstance().build(); + var errors = MappingErrors.root(); + + // act + var actual = expressionExtractor.getPermissionExpression(policy, errors); + + // assert + assertThat(actual.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); + verify(policyValidator).validateOtherPolicyFieldsUnset(policy, errors); + } + + @Test + void test_getPermissionConstraints_no_constraints() { + // arrange + var permission = Permission.Builder.newInstance() + .build(); + var policy = Policy.Builder.newInstance() + .permissions(List.of(permission)) + .build(); + var errors = MappingErrors.root(); + + // act + var actual = expressionExtractor.getPermissionExpression(policy, errors); + + // assert + assertThat(actual.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); + verify(policyValidator).validateOtherPolicyFieldsUnset(policy, errors); + } + + @Test + void test_getPermissionConstraints_single_constraint() { + // arrange + var constraint = mock(Constraint.class); + var permission = Permission.Builder.newInstance() + .constraint(constraint) + .build(); + + var policy = Policy.Builder.newInstance() + .permission(permission) + .build(); + var errors = MappingErrors.root(); + + var uiExpression = mock(UiPolicyExpression.class); + when(expressionMapper.buildUiPolicyExpressions(eq(List.of(constraint)), any())).thenReturn(List.of(uiExpression)); + + // act + var actual = expressionExtractor.getPermissionExpression(policy, errors); + + // assert + verify(policyValidator).validateOtherPermissionFieldsUnset(same(permission), any()); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(uiExpression); + assertThat(errors.getErrors()).isEmpty(); + } + + @Test + void test_getPermissionConstraints_merge_constraints() { + // arrange + var first = mock(Constraint.class); + var firstPermission = Permission.Builder.newInstance() + .constraint(first) + .build(); + + var second = mock(Constraint.class); + var secondPermission = Permission.Builder.newInstance() + .constraint(second) + .build(); + + var policy = Policy.Builder.newInstance() + .permission(firstPermission) + .permission(secondPermission) + .build(); + var errors = MappingErrors.root(); + + var firstUiExpression = mock(UiPolicyExpression.class); + var secondUiExpression = mock(UiPolicyExpression.class); + when(expressionMapper.buildUiPolicyExpressions(eq(List.of(first)), any())).thenReturn(List.of(firstUiExpression)); + when(expressionMapper.buildUiPolicyExpressions(eq(List.of(second)), any())).thenReturn(List.of(secondUiExpression)); + + // act + var actual = expressionExtractor.getPermissionExpression(policy, errors); + + // assert + verify(policyValidator).validateOtherPermissionFieldsUnset(same(firstPermission), any()); + verify(policyValidator).validateOtherPermissionFieldsUnset(same(secondPermission), any()); + var expected = UiPolicyExpression.and(List.of(firstUiExpression, secondUiExpression)); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + assertThat(errors.getErrors()).containsExactly( + "$: Multiple permissions were present. Prefer using a conjunction using AND." + ); + } + + @Test + void test_getPermissionConstraints_merge_constraints2() { + // arrange + var first = mock(Constraint.class); + var second = mock(Constraint.class); + var permission = Permission.Builder.newInstance() + .constraints(List.of(first, second)) + .build(); + + var policy = Policy.Builder.newInstance() + .permission(permission) + .build(); + + var errors = MappingErrors.root(); + + var firstUiExpression = mock(UiPolicyExpression.class); + var secondUiExpression = mock(UiPolicyExpression.class); + when(expressionMapper.buildUiPolicyExpressions(eq(List.of(first, second)), any())) + .thenReturn(List.of(firstUiExpression, secondUiExpression)); + + // act + var actual = expressionExtractor.getPermissionExpression(policy, errors); + + // assert + verify(policyValidator).validateOtherPermissionFieldsUnset(same(permission), any()); + var expected = UiPolicyExpression.and(List.of(firstUiExpression, secondUiExpression)); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + assertThat(errors.getErrors()).containsExactly( + "$.permissions[0].constraints: Multiple constraints were present. Prefer using a conjunction using AND." + ); + } + + @Test + void test_getPermissionConstraints_error_mapping() { + // arrange + var constraint = mock(Constraint.class); + var permission = Permission.Builder.newInstance() + .constraint(constraint) + .build(); + + var policy = Policy.Builder.newInstance() + .permission(permission) + .build(); + var errors = MappingErrors.root(); + + when(expressionMapper.buildUiPolicyExpressions(eq(List.of(constraint)), any())).thenAnswer(i -> { + i.getArgument(1, MappingErrors.class).add("test"); + return List.of(); + }); + + // act + var actual = expressionExtractor.getPermissionExpression(policy, errors); + + // assert + verify(policyValidator).validateOtherPermissionFieldsUnset(same(permission), any()); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(UiPolicyExpression.empty()); + assertThat(errors.getErrors()).containsExactlyInAnyOrder( + "$.permissions[0].constraints: test" + ); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapperTest.java new file mode 100644 index 000000000..6a481887c --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/ExpressionMapperTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers.policy; + +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import org.eclipse.edc.policy.model.AndConstraint; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.OrConstraint; +import org.eclipse.edc.policy.model.XoneConstraint; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExpressionMapperTest { + @InjectMocks + ExpressionMapper expressionMapper; + + @Mock + AtomicConstraintMapper atomicConstraintMapper; + + @Test + void buildUiConstraint_errorPropagation() { + // arrange + var errors = MappingErrors.root(); + var atomicConstraint = mock(AtomicConstraint.class); + when(atomicConstraintMapper.buildUiConstraint(same(atomicConstraint), any())) + .thenAnswer(i -> { + i.getArgument(1, MappingErrors.class).add("test"); + return Optional.empty(); + }); + + var constraints = List.of(atomicConstraint); + + // act + var actual = expressionMapper.buildUiPolicyExpressions(constraints, errors); + + // assert + assertThat(actual).isEmpty(); + assertThat(errors.getErrors()).containsExactly("$[0]: test"); + } + + @Test + void buildUiConstraint_simpleAtomicConstraint() { + // arrange + var atomicConstraint = mock(AtomicConstraint.class); + var uiConstraint = mock(UiPolicyConstraint.class); + when(atomicConstraintMapper.buildUiConstraint(same(atomicConstraint), any())) + .thenReturn(Optional.of(uiConstraint)); + + var constraints = List.of(atomicConstraint); + + // act + var actual = expressionMapper.buildUiPolicyExpressions(constraints, MappingErrors.root()); + + // assert + assertThat(actual).containsExactly( + UiPolicyExpression.constraint(uiConstraint) + ); + } + + @Test + void buildUiConstraint_andConstraint() { + // arrange + var atomicConstraint = mock(AtomicConstraint.class); + var uiConstraint = mock(UiPolicyConstraint.class); + when(atomicConstraintMapper.buildUiConstraint(same(atomicConstraint), any())) + .thenReturn(Optional.of(uiConstraint)); + + var constraints = List.of( + AndConstraint.Builder.newInstance() + .constraint(atomicConstraint) + .build() + ); + + // act + var actual = expressionMapper.buildUiPolicyExpressions(constraints, MappingErrors.root()); + + // assert + assertThat(actual).containsExactly( + UiPolicyExpression.and(List.of( + UiPolicyExpression.constraint(uiConstraint) + )) + ); + } + + @Test + void buildUiConstraint_orConstraint() { + // arrange + var atomicConstraint = mock(AtomicConstraint.class); + var uiConstraint = mock(UiPolicyConstraint.class); + when(atomicConstraintMapper.buildUiConstraint(same(atomicConstraint), any())) + .thenReturn(Optional.of(uiConstraint)); + + var constraints = List.of( + OrConstraint.Builder.newInstance() + .constraint(atomicConstraint) + .build() + ); + + // act + var actual = expressionMapper.buildUiPolicyExpressions(constraints, MappingErrors.root()); + + // assert + assertThat(actual).containsExactly( + UiPolicyExpression.or(List.of( + UiPolicyExpression.constraint(uiConstraint) + )) + ); + } + + @Test + void buildUiConstraint_xoneConstraint() { + // arrange + var atomicConstraint = mock(AtomicConstraint.class); + var uiConstraint = mock(UiPolicyConstraint.class); + when(atomicConstraintMapper.buildUiConstraint(same(atomicConstraint), any())) + .thenReturn(Optional.of(uiConstraint)); + + var constraints = List.of( + XoneConstraint.Builder.newInstance() + .constraint(atomicConstraint) + .build() + ); + + // act + var actual = expressionMapper.buildUiPolicyExpressions(constraints, MappingErrors.root()); + + // assert + assertThat(actual).containsExactly( + UiPolicyExpression.xone(List.of( + UiPolicyExpression.constraint(uiConstraint) + )) + ); + } + + @Test + void buildConstraint_atomicConstraint() { + // arrange + var uiConstraint = mock(UiPolicyConstraint.class); + var expression = UiPolicyExpression.constraint(uiConstraint); + + var atomicConstraint = mock(AtomicConstraint.class); + when(atomicConstraintMapper.buildAtomicConstraint(uiConstraint)) + .thenReturn(atomicConstraint); + + // act + var actual = expressionMapper.buildConstraints(List.of(expression)); + + // assert + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isEqualTo(atomicConstraint); + } + + @Test + void buildConstraint_andConstraint() { + // arrange + var uiConstraint = mock(UiPolicyConstraint.class); + var expression = UiPolicyExpression.and(List.of( + UiPolicyExpression.constraint(uiConstraint) + )); + + var atomicConstraint = mock(AtomicConstraint.class); + when(atomicConstraintMapper.buildAtomicConstraint(uiConstraint)) + .thenReturn(atomicConstraint); + + // act + var actual = expressionMapper.buildConstraints(List.of(expression)); + + // assert + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isInstanceOf(AndConstraint.class); + + var constraints = ((AndConstraint) actual.get(0)).getConstraints(); + assertThat(constraints).hasSize(1); + assertThat(constraints.get(0)).isEqualTo(atomicConstraint); + } + + @Test + void buildConstraint_orConstraint() { + // arrange + var uiConstraint = mock(UiPolicyConstraint.class); + var expression = UiPolicyExpression.or(List.of( + UiPolicyExpression.constraint(uiConstraint) + )); + + var atomicConstraint = mock(AtomicConstraint.class); + when(atomicConstraintMapper.buildAtomicConstraint(uiConstraint)) + .thenReturn(atomicConstraint); + + // act + var actual = expressionMapper.buildConstraints(List.of(expression)); + + // assert + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isInstanceOf(OrConstraint.class); + + var constraints = ((OrConstraint) actual.get(0)).getConstraints(); + assertThat(constraints).hasSize(1); + assertThat(constraints.get(0)).isEqualTo(atomicConstraint); + } + + @Test + void buildConstraint_xoneConstraint() { + // arrange + var uiConstraint = mock(UiPolicyConstraint.class); + var expression = UiPolicyExpression.xone(List.of( + UiPolicyExpression.constraint(uiConstraint) + )); + + var atomicConstraint = mock(AtomicConstraint.class); + when(atomicConstraintMapper.buildAtomicConstraint(uiConstraint)) + .thenReturn(atomicConstraint); + + // act + var actual = expressionMapper.buildConstraints(List.of(expression)); + + // assert + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isInstanceOf(XoneConstraint.class); + + var constraints = ((XoneConstraint) actual.get(0)).getConstraints(); + assertThat(constraints).hasSize(1); + assertThat(constraints.get(0)).isEqualTo(atomicConstraint); + } +} diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 9613f64e1..d554dd74f 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.LegacyPolicyMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; @@ -28,7 +29,8 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpDataSourceMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http.HttpHeaderMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.AtomicConstraintMapper; -import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ConstraintExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionExtractor; +import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.LiteralMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.OperatorMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.PolicyValidator; @@ -143,18 +145,11 @@ public static WrapperExtensionContext buildContext( var criterionOperatorMapper = new CriterionOperatorMapper(); var criterionLiteralMapper = new CriterionLiteralMapper(); var criterionMapper = new CriterionMapper(criterionOperatorMapper, criterionLiteralMapper); - var literalMapper = new LiteralMapper(objectMapper); - var atomicConstraintMapper = new AtomicConstraintMapper(literalMapper, operatorMapper); - var policyValidator = new PolicyValidator(); - var constraintExtractor = new ConstraintExtractor(policyValidator, atomicConstraintMapper); - var policyMapper = new PolicyMapper( - constraintExtractor, - atomicConstraintMapper, - typeTransformerRegistry); var edcPropertyUtils = new EdcPropertyUtils(); var selfDescriptionService = new SelfDescriptionService(config, monitor); var ownConnectorEndpointService = new OwnConnectorEndpointServiceImpl(selfDescriptionService); var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd, ownConnectorEndpointService); + var policyMapper = newPolicyMapper(objectMapper, typeTransformerRegistry, operatorMapper); var transferProcessStateService = new TransferProcessStateService(); var contractNegotiationUtils = new ContractNegotiationUtils( contractNegotiationService, @@ -218,9 +213,11 @@ public static WrapperExtensionContext buildContext( var agreementDetailsQuery = new ContractAgreementTerminationDetailsQuery(); var terminateContractQuery = new TerminateContractQuery(); var contractAgreementTerminationApiService = new ContractAgreementTerminationApiService(contractAgreementTerminationService); + var legacyPolicyMapper = new LegacyPolicyMapper(); var policyDefinitionApiService = new PolicyDefinitionApiService( policyDefinitionService, - policyMapper + policyMapper, + legacyPolicyMapper ); var dataOfferBuilder = new DspDataOfferBuilder(jsonLd); var uiDataOfferBuilder = new UiDataOfferBuilder(assetMapper, policyMapper); @@ -295,8 +292,7 @@ public static WrapperExtensionContext buildContext( var useCaseResource = new UseCaseResourceImpl( kpiApiService, supportedPolicyApiService, - useCaseCatalogApiService, - policyDefinitionApiService + useCaseCatalogApiService ); // Collect all JAX-RS resources @@ -339,4 +335,22 @@ private static AssetMapper newAssetMapper( jsonLd ); } + + @NotNull + private static PolicyMapper newPolicyMapper( + ObjectMapper objectMapper, + TypeTransformerRegistry typeTransformerRegistry, + OperatorMapper operatorMapper + ) { + var literalMapper = new LiteralMapper(objectMapper); + var atomicConstraintMapper = new AtomicConstraintMapper(literalMapper, operatorMapper); + var policyValidator = new PolicyValidator(); + var expressionMapper = new ExpressionMapper(atomicConstraintMapper); + var constraintExtractor = new ExpressionExtractor(policyValidator, expressionMapper); + return new PolicyMapper( + constraintExtractor, + expressionMapper, + typeTransformerRegistry + ); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 1d7af06fe..e43a992fd 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -14,7 +14,8 @@ package de.sovity.edc.ext.wrapper.api.ui; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; @@ -103,10 +104,16 @@ public PolicyDefinitionPage getPolicyDefinitionPage() { } @Override + @Deprecated public IdResponseDto createPolicyDefinition(PolicyDefinitionCreateRequest policyDefinitionDtoDto) { return policyDefinitionApiService.createPolicyDefinition(policyDefinitionDtoDto); } + @Override + public IdResponseDto createPolicyDefinitionV2(PolicyDefinitionCreateDto policyDefinitionCreateDto) { + return policyDefinitionApiService.createPolicyDefinitionV2(policyDefinitionCreateDto); + } + @Override public IdResponseDto deletePolicyDefinition(String policyId) { return policyDefinitionApiService.deletePolicyDefinition(policyId); diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java index 0ce0b737e..87a17cbcb 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiService.java @@ -16,11 +16,13 @@ import de.sovity.edc.ext.wrapper.api.ServiceException; +import de.sovity.edc.ext.wrapper.api.common.mappers.LegacyPolicyMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionCreateRequest; -import de.sovity.edc.ext.wrapper.api.common.model.PolicyDefinitionDto; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; -import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionDto; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; @@ -36,18 +38,28 @@ public class PolicyDefinitionApiService { private final PolicyDefinitionService policyDefinitionService; private final PolicyMapper policyMapper; + private final LegacyPolicyMapper legacyPolicyMapper; public List getPolicyDefinitions() { var policyDefinitions = getAllPolicyDefinitions(); return policyDefinitions.stream() - .sorted(Comparator.comparing(PolicyDefinition::getCreatedAt).reversed()) - .map(this::buildPolicyDefinitionDto) - .toList(); + .sorted(Comparator.comparing(PolicyDefinition::getCreatedAt).reversed()) + .map(this::buildPolicyDefinitionDto) + .toList(); } @NotNull + @Deprecated public IdResponseDto createPolicyDefinition(PolicyDefinitionCreateRequest request) { - var policyDefinition = buildPolicyDefinition(request); + var uiPolicyExpression = legacyPolicyMapper.buildUiPolicyExpression(request.getPolicy()); + var policyDefinition = buildPolicyDefinition(request.getPolicyDefinitionId(), uiPolicyExpression); + policyDefinition = policyDefinitionService.create(policyDefinition).orElseThrow(ServiceException::new); + return new IdResponseDto(policyDefinition.getId()); + } + + @NotNull + public IdResponseDto createPolicyDefinitionV2(PolicyDefinitionCreateDto request) { + var policyDefinition = buildPolicyDefinition(request.getPolicyDefinitionId(), request.getExpression()); policyDefinition = policyDefinitionService.create(policyDefinition).orElseThrow(ServiceException::new); return new IdResponseDto(policyDefinition.getId()); } @@ -61,34 +73,20 @@ public IdResponseDto deletePolicyDefinition(String policyDefinitionId) { private List getAllPolicyDefinitions() { return policyDefinitionService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); } + public PolicyDefinitionDto buildPolicyDefinitionDto(PolicyDefinition policyDefinition) { var policy = policyMapper.buildUiPolicy(policyDefinition.getPolicy()); return PolicyDefinitionDto.builder() - .policyDefinitionId(policyDefinition.getId()) - .policy(policy) - .build(); - } - - public PolicyDefinition buildPolicyDefinition(PolicyDefinitionCreateRequest policyDefinitionDto) { - var policy = policyMapper.buildPolicy(policyDefinitionDto.getPolicy()); - return PolicyDefinition.Builder.newInstance() - .id(policyDefinitionDto.getPolicyDefinitionId()) - .policy(policy) - .build(); - } - - public IdResponseDto createPolicyDefinition(PolicyCreateRequest policyCreateRequest) { - var policyDefinition = buildPolicyDefinition(policyCreateRequest); - policyDefinition = policyDefinitionService.create(policyDefinition).orElseThrow(ServiceException::new); - return new IdResponseDto(policyDefinition.getId()); + .policyDefinitionId(policyDefinition.getId()) + .policy(policy) + .build(); } - private PolicyDefinition buildPolicyDefinition(PolicyCreateRequest policyCreateRequest) { - var permissionExpression = policyCreateRequest.getPermission().getExpression(); - var policy = policyMapper.buildPolicy(List.of(permissionExpression)); + public PolicyDefinition buildPolicyDefinition(String id, UiPolicyExpression uiPolicyExpression) { + var policy = policyMapper.buildPolicy(uiPolicyExpression); return PolicyDefinition.Builder.newInstance() - .id(policyCreateRequest.getPolicyDefinitionId()) - .policy(policy) - .build(); + .id(id) + .policy(policy) + .build(); } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java index 076c0db12..7f1bf7d74 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResourceImpl.java @@ -14,12 +14,9 @@ package de.sovity.edc.ext.wrapper.api.usecase; -import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.UiDataOffer; -import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import de.sovity.edc.ext.wrapper.api.usecase.model.CatalogQuery; import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; -import de.sovity.edc.ext.wrapper.api.usecase.model.PolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; @@ -36,7 +33,6 @@ public class UseCaseResourceImpl implements UseCaseResource { private final KpiApiService kpiApiService; private final SupportedPolicyApiService supportedPolicyApiService; private final UseCaseCatalogApiService useCaseCatalogApiService; - private final PolicyDefinitionApiService policyDefinitionApiService; @Override public KpiResult getKpis() { @@ -52,9 +48,4 @@ public List getSupportedFunctions() { public List queryCatalog(CatalogQuery catalogQuery) { return useCaseCatalogApiService.fetchDataOffers(catalogQuery); } - - @Override - public IdResponseDto createPolicyDefinitionUseCase(PolicyCreateRequest policyCreateRequest) { - return policyDefinitionApiService.createPolicyDefinition(policyCreateRequest); - } } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java index fa71fe209..101a76fdb 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/catalog/CatalogApiTest.java @@ -17,7 +17,7 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.DataSourceType; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; @@ -25,7 +25,8 @@ import de.sovity.edc.client.gen.model.UiCriterionOperator; import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; @@ -107,14 +108,12 @@ private void createAsset() { } private void createPolicy() { - var policyDefinition = PolicyDefinitionCreateRequest.builder() + var policyDefinition = PolicyDefinitionCreateDto.builder() .policyDefinitionId(dataOfferId) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) - .build()) + .expression(UiPolicyExpression.builder().type(UiPolicyExpressionType.EMPTY).build()) .build(); - client.uiApi().createPolicyDefinition(policyDefinition); + client.uiApi().createPolicyDefinitionV2(policyDefinition); } private void createContractDefinition() { diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java index 9b6536366..a69824768 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java @@ -18,6 +18,7 @@ import de.sovity.edc.client.gen.model.ContractAgreementDirection; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; @@ -121,7 +122,10 @@ void testContractAgreementPage( assertThat(transfer.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); assertThat(transfer.getErrorMessage()).isEqualTo("my-error-message-1"); - var constraint = agreement.getContractPolicy().getConstraints().get(0); + var expression = agreement.getContractPolicy().getExpression(); + assertThat(expression.getType()).isEqualTo(UiPolicyExpressionType.CONSTRAINT); + + var constraint = expression.getConstraint(); assertThat(constraint.getLeft()).isEqualTo("ALWAYS_TRUE"); assertThat(constraint.getOperator()).isEqualTo(OperatorDto.EQ); assertThat(constraint.getRight().getValue()).isEqualTo("true"); diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java index d80ac3cbd..f6e3344df 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/policy/PolicyDefinitionApiServiceTest.java @@ -17,17 +17,17 @@ import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.PolicyDefinitionDto; import de.sovity.edc.client.gen.model.UiPolicyConstraint; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.ext.db.jooq.Tables; import de.sovity.edc.extension.db.directaccess.DslContextFactory; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; -import lombok.SneakyThrows; import lombok.val; import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.junit.annotations.ApiTest; @@ -37,7 +37,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import java.util.List; import java.util.Map; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; @@ -62,12 +61,15 @@ class PolicyDefinitionApiServiceTest { } ); - UiPolicyConstraint constraint = UiPolicyConstraint.builder() - .left("a") - .operator(OperatorDto.EQ) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value("b") + UiPolicyExpression expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() + .left("a") + .operator(OperatorDto.EQ) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value("b") + .build()) .build()) .build(); @@ -86,10 +88,7 @@ void getPolicyList() { .filter(it -> it.getPolicyDefinitionId().equals("my-policy-def-1")) .findFirst().get(); assertThat(policyDefinition.getPolicyDefinitionId()).isEqualTo("my-policy-def-1"); - assertThat(policyDefinition.getPolicy().getConstraints()).hasSize(1); - - var constraintEntry = policyDefinition.getPolicy().getConstraints().get(0); - assertThat(constraintEntry).usingRecursiveComparison().isEqualTo(constraint); + assertThat(policyDefinition.getPolicy().getExpression()).usingRecursiveComparison().isEqualTo(expression); } @Test @@ -144,9 +143,8 @@ void test_delete(PolicyDefinitionService policyDefinitionService) { } private void createPolicyDefinition(String policyDefinitionId) { - var policy = new UiPolicyCreateRequest(List.of(constraint)); - var policyDefinition = new PolicyDefinitionCreateRequest(policyDefinitionId, policy); - client.uiApi().createPolicyDefinition(policyDefinition); + var policyDefinition = new PolicyDefinitionCreateDto(policyDefinitionId, expression); + client.uiApi().createPolicyDefinitionV2(policyDefinition); } } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java deleted file mode 100644 index a6215b6ad..000000000 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/PolicyDefinitionApiServiceTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package de.sovity.edc.ext.wrapper.api.usecase; - -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.client.gen.model.AtomicConstraintDto; -import de.sovity.edc.client.gen.model.Expression; -import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PermissionDto; -import de.sovity.edc.client.gen.model.PolicyCreateRequest; -import de.sovity.edc.client.gen.model.PolicyDefinitionDto; -import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; -import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import org.eclipse.edc.junit.annotations.ApiTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.io.StringReader; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static de.sovity.edc.client.gen.model.ExpressionType.AND; -import static de.sovity.edc.client.gen.model.ExpressionType.ATOMIC_CONSTRAINT; -import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -@ApiTest -public class PolicyDefinitionApiServiceTest { - - private static ConnectorConfig config; - private static EdcClient client; - - @RegisterExtension - static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( - ":launchers:connectors:sovity-dev", - "provider", - testDatabase -> { - config = forTestDatabase("my-edc-participant-id", testDatabase); - client = EdcClient.builder() - .managementApiUrl(config.getManagementEndpoint().getUri().toString()) - .managementApiKey(config.getProperties().get("edc.api.auth.key")) - .build(); - return config.getProperties(); - } - ); - - @Test - void createTraceXPolicy() { - // arrange - var policyId = UUID.randomUUID().toString(); - var membershipElement = buildAtomicElement("Membership", OperatorDto.EQ, "active"); - var purposeElement = buildAtomicElement("PURPOSE", OperatorDto.EQ, "ID 3.1 Trace"); - var andElement = new Expression() - .expressionType(AND) - .expressions(List.of(membershipElement, purposeElement)); - var permissionDto = new PermissionDto(andElement); - var createRequest = new PolicyCreateRequest(policyId, permissionDto); - - // act - var response = client.useCaseApi().createPolicyDefinitionUseCase(createRequest); - - // assert - assertThat(response.getId()).isEqualTo(policyId); - var policyById = getPolicyById(policyId); - assertThat(policyById).isPresent(); - var policyDefinitionDto = policyById.get(); - assertEquals(policyId, policyDefinitionDto.getPolicyDefinitionId()); - assertPolicyJsonLd(policyDefinitionDto); - } - - private void assertPolicyJsonLd(PolicyDefinitionDto policyDefinitionDto) { - var permission = getPermissionJsonObject(policyDefinitionDto.getPolicy().getPolicyJsonLd()); - var action = permission.get(Prop.Odrl.ACTION); - assertEquals(Prop.Odrl.USE, action.asJsonObject().getString(Prop.Odrl.TYPE)); - - var permissionConstraints = permission.get(Prop.Odrl.CONSTRAINT).asJsonArray(); - assertThat(permissionConstraints).hasSize(1); - var andConstraint = permissionConstraints.get(0).asJsonObject(); - var andConstraints = andConstraint.get(Prop.Odrl.AND).asJsonArray(); - assertThat(andConstraints).hasSize(2); - - var membershipConstraint = andConstraints.get(0).asJsonObject(); - var purposeConstraint = andConstraints.get(1).asJsonObject(); - assertAtomicConstraint(membershipConstraint, "Membership", "active"); - assertAtomicConstraint(purposeConstraint, "PURPOSE", "ID 3.1 Trace"); - } - - private static JsonObject getPermissionJsonObject(String policyJsonLdString) { - var jsonReader = Json.createReader(new StringReader(policyJsonLdString)); - var jsonObject = jsonReader.readObject(); - var permissionList = jsonObject.get(Prop.Odrl.PERMISSION); - return permissionList.asJsonArray().get(0).asJsonObject(); - } - - private void assertAtomicConstraint(JsonObject atomicConstraint, String left, String right) { - var leftOperand = atomicConstraint.getJsonObject(Prop.Odrl.LEFT_OPERAND); - assertEquals(left, leftOperand.getString("@value")); - var rightOperand = atomicConstraint.getJsonObject(Prop.Odrl.RIGHT_OPERAND); - assertEquals(right, rightOperand.getString("@value")); - } - - private Expression buildAtomicElement( - String left, - OperatorDto operator, - String right) { - var atomicConstraint = new AtomicConstraintDto() - .leftExpression(left) - .operator(operator) - .rightExpression(right); - return new Expression() - .expressionType(ATOMIC_CONSTRAINT) - .atomicConstraint(atomicConstraint); - } - - private Optional getPolicyById(String policyId) { - var policyDefinitionsResponse = client.uiApi().getPolicyDefinitionPage(); - return policyDefinitionsResponse.getPolicies().stream() - .filter(policy -> policy.getPolicyDefinitionId().equals(policyId)) - .findFirst(); - } -} diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java index e8cefe982..4a4dbfd43 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseApiWrapperTest.java @@ -22,7 +22,7 @@ import de.sovity.edc.client.gen.model.CatalogQuery; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.DataSourceType; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; @@ -30,7 +30,8 @@ import de.sovity.edc.client.gen.model.UiCriterionOperator; import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; @@ -201,11 +202,11 @@ private void setupAssets() { .mediaType("application/json") .build()).getId(); - policyId = client.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() + policyId = client.uiApi().createPolicyDefinitionV2(PolicyDefinitionCreateDto.builder() .policyDefinitionId("policy-1") - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) - .build()) + .expression(UiPolicyExpression.builder() + .type(UiPolicyExpressionType.EMPTY) + .build()) .build()).getId(); } } diff --git a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java index 436be683b..71143851d 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java @@ -21,7 +21,7 @@ import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; import de.sovity.edc.client.gen.model.UiContractOffer; @@ -33,7 +33,8 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; @@ -146,32 +147,41 @@ private void createAsset() { } private void createPolicy() { - var afterYesterday = UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.GT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(OffsetDateTime.now().minusDays(1).toString()) + var afterYesterday = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().minusDays(1).toString()) + .build()) .build()) .build(); - var beforeTomorrow = UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.LT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(OffsetDateTime.now().plusDays(1).toString()) + var beforeTomorrow = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.LT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().plusDays(1).toString()) + .build()) .build()) .build(); - var policyDefinition = PolicyDefinitionCreateRequest.builder() + var expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(List.of(afterYesterday, beforeTomorrow)) + .build(); + + var policyDefinition = PolicyDefinitionCreateDto.builder() .policyDefinitionId(dataOfferId) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(afterYesterday, beforeTomorrow)) - .build()) + .expression(expression) .build(); - providerClient.uiApi().createPolicyDefinition(policyDefinition); + providerClient.uiApi().createPolicyDefinitionV2(policyDefinition); } private void createContractDefinition() { diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java index 9df67d5d7..f0bdd6ea3 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -21,7 +21,7 @@ import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.TransferHistoryEntry; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; @@ -33,7 +33,8 @@ import de.sovity.edc.client.gen.model.UiDataOffer; import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; import de.sovity.edc.extension.e2e.extension.Consumer; @@ -448,14 +449,14 @@ private String createAssetWithParameterizedMethod(EdcClient providerClient, Test } private void createPolicy(EdcClient providerClient, TestCase testCase) { - var policyDefinition = PolicyDefinitionCreateRequest.builder() + var policyDefinition = PolicyDefinitionCreateDto.builder() .policyDefinitionId(testCase.id) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) + .expression(UiPolicyExpression.builder() + .type(UiPolicyExpressionType.EMPTY) .build()) .build(); - providerClient.uiApi().createPolicyDefinition(policyDefinition); + providerClient.uiApi().createPolicyDefinitionV2(policyDefinition); } private String createContractDefinition(EdcClient providerClient, TestCase testCase) { @@ -550,4 +551,5 @@ private String initiateTransferWithParameters( return consumerClient.uiApi().initiateTransfer(transferRequest).getId(); } + } diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index 9c4e79360..790e0c268 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -22,7 +22,7 @@ import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiAssetEditRequest; @@ -36,7 +36,8 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; @@ -102,20 +103,21 @@ void provide_consume_assetMapping_policyMapping_agreements( var data = "expected data 123"; var yesterday = OffsetDateTime.now().minusDays(1); - var constraintRequest = UiPolicyConstraint.builder() - .left("POLICY_EVALUATION_TIME") - .operator(OperatorDto.GT) - .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(yesterday.toString()) + var expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() + .left("POLICY_EVALUATION_TIME") + .operator(OperatorDto.GT) + .right(UiPolicyLiteral.builder() + .type(UiPolicyLiteralType.STRING) + .value(yesterday.toString()) + .build()) .build()) .build(); - var policyId = providerClient.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() + var policyId = providerClient.uiApi().createPolicyDefinitionV2(PolicyDefinitionCreateDto.builder() .policyDefinitionId("policy-1") - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(constraintRequest)) - .build()) + .expression(expression) .build()).getId(); var dataSource = UiDataSource.builder() @@ -276,8 +278,8 @@ void provide_consume_assetMapping_policyMapping_agreements( assertThat(providerAgreement.getCounterPartyId()).isEqualTo(CONSUMER_PARTICIPANT_ID); assertThat(providerAgreement.getAsset().getAssetId()).isEqualTo(assetId); - var providingContractPolicyConstraint = providerAgreement.getContractPolicy().getConstraints().get(0); - assertThat(providingContractPolicyConstraint).usingRecursiveComparison().isEqualTo(providingContractPolicyConstraint); + var providingContractPolicyConstraint = providerAgreement.getContractPolicy().getExpression(); + assertThat(providingContractPolicyConstraint).usingRecursiveComparison().isEqualTo(expression); assertThat(providerAgreement.getAsset().getAssetId()).isEqualTo(assetId); assertThat(providerAgreement.getAsset().getKeywords()).isEqualTo(List.of("keyword1", "keyword2")); @@ -291,15 +293,15 @@ void provide_consume_assetMapping_policyMapping_agreements( assertThat(consumerAgreement.getCounterPartyId()).isEqualTo(PROVIDER_PARTICIPANT_ID); assertThat(consumerAgreement.getAsset().getAssetId()).isEqualTo(assetId); - var consumingContractPolicyConstraint = consumerAgreement.getContractPolicy().getConstraints().get(0); - assertThat(consumingContractPolicyConstraint).usingRecursiveComparison().isEqualTo(consumingContractPolicyConstraint); + var consumingContractPolicyConstraint = consumerAgreement.getContractPolicy().getExpression(); + assertThat(consumingContractPolicyConstraint).usingRecursiveComparison().isEqualTo(expression); assertThat(consumerAgreement.getAsset().getAssetId()).isEqualTo(assetId); assertThat(consumerAgreement.getAsset().getTitle()).isEqualTo(assetId); // Test Policy - assertThat(contractOffer.getPolicy().getConstraints()).hasSize(1); - var constraint = contractOffer.getPolicy().getConstraints().get(0); + assertThat(contractOffer.getPolicy().getExpression().getType()).isEqualTo(UiPolicyExpressionType.CONSTRAINT); + var constraint = contractOffer.getPolicy().getExpression().getConstraint(); assertThat(constraint.getLeft()).isEqualTo("POLICY_EVALUATION_TIME"); assertThat(constraint.getOperator()).isEqualTo(OperatorDto.GT); assertThat(constraint.getRight().getType()).isEqualTo(UiPolicyLiteralType.STRING); @@ -381,10 +383,10 @@ void customTransferRequest( .build()).getId(); assertThat(assetId).isEqualTo("asset-1"); - var policyId = providerClient.uiApi().createPolicyDefinition(PolicyDefinitionCreateRequest.builder() + var policyId = providerClient.uiApi().createPolicyDefinitionV2(PolicyDefinitionCreateDto.builder() .policyDefinitionId("policy-1") - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of()) + .expression(UiPolicyExpression.builder() + .type(UiPolicyExpressionType.EMPTY) .build()) .build()).getId(); diff --git a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java index 2e8fa6630..3231fda3d 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java @@ -23,7 +23,7 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiCriterion; import de.sovity.edc.client.gen.model.UiCriterionLiteral; @@ -32,7 +32,8 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.extension.e2e.connector.ConnectorRemote; @@ -96,17 +97,19 @@ private CatalogQuery criterion( String rightOperand) { return CatalogQuery.builder() - .connectorEndpoint(getProtocolEndpoint(providerConnector)) - .filterExpressions( - List.of( - CatalogFilterExpression.builder() - .operandLeft(leftOperand) - .operator(operator) - .operandRight(CatalogFilterExpressionLiteral.builder().value(rightOperand).type(CatalogFilterExpressionLiteralType.VALUE).build()) - .build() - ) + .connectorEndpoint(getProtocolEndpoint(providerConnector)) + .filterExpressions( + List.of( + CatalogFilterExpression.builder() + .operandLeft(leftOperand) + .operator(operator) + .operandRight( + CatalogFilterExpressionLiteral.builder().value(rightOperand).type(CatalogFilterExpressionLiteralType.VALUE) + .build()) + .build() ) - .build(); + ) + .build(); } private void createAsset(EdcClient providerClient) { @@ -132,48 +135,57 @@ private void createAsset(EdcClient providerClient) { } private void createPolicy(EdcClient providerClient) { - var afterYesterday = UiPolicyConstraint.builder() + var afterYesterday = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() .left("POLICY_EVALUATION_TIME") .operator(OperatorDto.GT) .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(OffsetDateTime.now().minusDays(1).toString()) - .build()) - .build(); + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().minusDays(1).toString()) + .build()) + .build()) + .build(); - var beforeTomorrow = UiPolicyConstraint.builder() + var beforeTomorrow = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(UiPolicyConstraint.builder() .left("POLICY_EVALUATION_TIME") .operator(OperatorDto.LT) .right(UiPolicyLiteral.builder() - .type(UiPolicyLiteralType.STRING) - .value(OffsetDateTime.now().plusDays(1).toString()) - .build()) - .build(); - - var policyDefinition = PolicyDefinitionCreateRequest.builder() - .policyDefinitionId(dataOfferId) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(afterYesterday, beforeTomorrow)) - .build()) - .build(); - - providerClient.uiApi().createPolicyDefinition(policyDefinition); + .type(UiPolicyLiteralType.STRING) + .value(OffsetDateTime.now().plusDays(1).toString()) + .build()) + .build()) + .build(); + + var expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(List.of(afterYesterday, beforeTomorrow)) + .build(); + + var policyDefinition = PolicyDefinitionCreateDto.builder() + .policyDefinitionId(dataOfferId) + .expression(expression) + .build(); + + providerClient.uiApi().createPolicyDefinitionV2(policyDefinition); } private void createContractDefinition(EdcClient providerClient) { var contractDefinition = ContractDefinitionRequest.builder() - .contractDefinitionId(dataOfferId) - .accessPolicyId(dataOfferId) - .contractPolicyId(dataOfferId) - .assetSelector(List.of(UiCriterion.builder() - .operandLeft(Prop.Edc.ID) - .operator(UiCriterionOperator.EQ) - .operandRight(UiCriterionLiteral.builder() - .type(UiCriterionLiteralType.VALUE) - .value(dataOfferId) - .build()) - .build())) - .build(); + .contractDefinitionId(dataOfferId) + .accessPolicyId(dataOfferId) + .contractPolicyId(dataOfferId) + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(dataOfferId) + .build()) + .build())) + .build(); providerClient.uiApi().createContractDefinition(contractDefinition); } diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java index a4bc6317d..346e256e6 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java @@ -24,7 +24,7 @@ import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.PolicyDefinitionCreateRequest; +import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; import de.sovity.edc.client.gen.model.UiCriterion; @@ -34,7 +34,8 @@ import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; import de.sovity.edc.client.gen.model.UiPolicyConstraint; -import de.sovity.edc.client.gen.model.UiPolicyCreateRequest; +import de.sovity.edc.client.gen.model.UiPolicyExpression; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; import de.sovity.edc.client.gen.model.UiPolicyLiteral; import de.sovity.edc.client.gen.model.UiPolicyLiteralType; import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; @@ -51,6 +52,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.RUNNING; import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; @@ -144,15 +146,22 @@ public String createPolicyDefinition(String policyId, UiPolicyConstraint... cons } private IdResponseDto createPolicyDefinition(String policyId, List constraints) { - var policyDefinition = PolicyDefinitionCreateRequest.builder() + var expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(constraints.stream() + .map(it -> UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(it) + .build()) + .toList()) + .build(); + + var policyDefinition = PolicyDefinitionCreateDto.builder() .policyDefinitionId(policyId) - .policy(UiPolicyCreateRequest.builder() - .constraints(constraints) - .build() - ) + .expression(expression) .build(); - return providerClient.uiApi().createPolicyDefinition(policyDefinition); + return providerClient.uiApi().createPolicyDefinitionV2(policyDefinition); } public String createContractDefinition(String assetId) { @@ -226,14 +235,22 @@ public void createPolicy(String id, OffsetDateTime from, OffsetDateTime until) { .build()) .build(); - var policyDefinition = PolicyDefinitionCreateRequest.builder() + val expression = UiPolicyExpression.builder() + .type(UiPolicyExpressionType.AND) + .expressions(Stream.of(startConstraint, endConstraint) + .map(it -> UiPolicyExpression.builder() + .type(UiPolicyExpressionType.CONSTRAINT) + .constraint(it) + .build()) + .toList()) + .build(); + + var policyDefinition = PolicyDefinitionCreateDto.builder() .policyDefinitionId(id) - .policy(UiPolicyCreateRequest.builder() - .constraints(List.of(startConstraint, endConstraint)) - .build()) + .expression(expression) .build(); - providerClient.uiApi().createPolicyDefinition(policyDefinition); + providerClient.uiApi().createPolicyDefinitionV2(policyDefinition); } public String transferAndAwait(InitiateTransferRequest transferRequest) { From 36bb35e026fbea06588dd380966b32760ea3170b Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 24 Jul 2024 13:29:59 +0200 Subject: [PATCH 268/295] feat: on request assets now return placeholder data (#1004) Co-authored-by: Richard Treier --- CHANGELOG.md | 6 -- build.gradle.kts | 4 + .../ext/catalog/crawler/CrawlerE2eTest.java | 1 + .../ext/catalog/crawler/CrawlerExtension.java | 14 +-- .../CrawlerExtensionContextBuilder.java | 11 ++- .../contract-termination/build.gradle.kts | 2 +- .../query/TerminateContractQueryTest.java | 2 + .../build.gradle.kts | 2 + .../policy-time-interval/build.gradle.kts | 2 + extensions/postgres-flyway/build.gradle.kts | 1 + extensions/sovity-messenger/build.gradle.kts | 1 - .../ui/model/ContractAgreementPageQuery.java | 1 - .../wrapper/api/usecase/UseCaseResource.java | 1 - .../wrapper-common-mappers/build.gradle.kts | 4 + .../mappers/PlaceholderEndpointService.java | 35 +++++++ .../http/HttpDataSourceMapper.java | 8 +- .../wrapper/api/common/mappers/Factory.java | 2 +- .../mappers/asset/AssetJsonLdBuilderTest.java | 2 +- extensions/wrapper/wrapper/build.gradle.kts | 1 + .../edc/ext/wrapper/WrapperExtension.java | 15 ++- .../ext/wrapper/WrapperExtensionContext.java | 6 +- .../WrapperExtensionContextBuilder.java | 29 +++--- .../services/ContractAgreementData.java | 1 - .../ContractAgreementPageCardBuilder.java | 2 - .../PlaceholderEndpointController.java | 47 +++++++++ gradle/libs.versions.toml | 1 + launchers/.env.connector | 4 + tests/build.gradle.kts | 1 + .../edc/e2e/ContractTerminationTest.java | 3 + .../PlaceholderDataSourceExtensionTest.java | 99 +++++++++++++++++++ utils/catalog-parser/build.gradle.kts | 1 + utils/test-utils/build.gradle.kts | 1 + .../config/ConnectorConfigFactory.java | 4 +- .../EdcRuntimeExtensionWithTestDatabase.java | 21 +++- .../extension/e2e/extension/E2eScenario.java | 37 +++---- .../e2e/extension/E2eTestExtension.java | 3 +- utils/versions/build.gradle.kts | 4 +- 37 files changed, 296 insertions(+), 83 deletions(-) create mode 100644 extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PlaceholderEndpointService.java create mode 100644 extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/controller/PlaceholderEndpointController.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index de3b23884..b04aaaea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,6 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Deployment Migration Notes -New configuration to access the database: - -- `EDC_SERVER_DB_CONNECTION_POOL_SIZE` - - The property controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. Basically this value will determine the maximum number of actual connections to the database backend. - - Defaults to `3` - #### Compatible Versions - Connector Backend Docker Images: diff --git a/build.gradle.kts b/build.gradle.kts index f9ff62348..9ee6fa69e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,6 +45,10 @@ allprojects { apply(plugin = "java") apply(plugin = "checkstyle") + configurations.all { + resolutionStrategy.force("org.eclipse.edc:runtime-metamodel:0.2.1") + } + tasks.withType { options.encoding = "UTF-8" sourceCompatibility = JavaVersion.VERSION_17.toString() diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java index 759f65cf4..1882f283a 100644 --- a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java @@ -106,6 +106,7 @@ class CrawlerE2eTest { props.put(CrawlerExtension.CRON_DEAD_CONNECTOR_REFRESH, everySeconds); props.put(CrawlerExtension.SCHEDULED_KILL_OFFLINE_CONNECTORS, everySeconds); props.put(CrawlerExtension.KILL_OFFLINE_CONNECTORS_AFTER, "P1D"); + props.put("my.edc.datasource.placeholder.baseurl", "http://example.com/edc/backend"); return props; } diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java index 290c5c2c3..ae1f0eca4 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtension.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.catalog.crawler; +import de.sovity.edc.ext.wrapper.api.common.mappers.PlaceholderEndpointService; import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; import org.eclipse.edc.connector.spi.catalog.CatalogService; import org.eclipse.edc.jsonld.spi.JsonLd; @@ -117,12 +118,13 @@ public void initialize(ServiceExtensionContext context) { } services = CrawlerExtensionContextBuilder.buildContext( - context.getConfig(), - context.getMonitor(), - typeManager, - typeTransformerRegistry, - jsonLd, - catalogService + context.getConfig(), + context.getMonitor(), + typeManager, + typeTransformerRegistry, + jsonLd, + catalogService, + new PlaceholderEndpointService("http://0.0.0.0/") ); // Provide access for the tests diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java index f95463a9f..89aa9e491 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/CrawlerExtensionContextBuilder.java @@ -52,6 +52,7 @@ import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.QuartzScheduleInitializer; import de.sovity.edc.ext.catalog.crawler.orchestration.schedules.utils.CronJobRef; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PlaceholderEndpointService; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; @@ -101,7 +102,8 @@ public static CrawlerExtensionContext buildContext( TypeManager typeManager, TypeTransformerRegistry typeTransformerRegistry, JsonLd jsonLd, - CatalogService catalogService + CatalogService catalogService, + PlaceholderEndpointService placeholderEndpointService ) { // Config var crawlerConfigFactory = new CrawlerConfigFactory(config); @@ -120,7 +122,7 @@ public static CrawlerExtensionContext buildContext( // Services var objectMapperJsonLd = getJsonLdObjectMapper(typeManager); - var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd); + var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd, placeholderEndpointService); var policyMapper = newPolicyMapper(typeTransformerRegistry, objectMapperJsonLd); var crawlerEventLogger = new CrawlerEventLogger(); var crawlerExecutionTimeLogger = new CrawlerExecutionTimeLogger(); @@ -229,7 +231,8 @@ private static PolicyMapper newPolicyMapper( @NotNull private static AssetMapper newAssetMapper( TypeTransformerRegistry typeTransformerRegistry, - JsonLd jsonLd + JsonLd jsonLd, + PlaceholderEndpointService placeholderEndpointService ) { var edcPropertyUtils = new EdcPropertyUtils(); var assetJsonLdUtils = new AssetJsonLdUtils(); @@ -241,7 +244,7 @@ private static AssetMapper newAssetMapper( endpoint -> false ); var httpHeaderMapper = new HttpHeaderMapper(); - var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); + var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper, placeholderEndpointService); var dataSourceMapper = new DataSourceMapper( edcPropertyUtils, httpDataSourceMapper diff --git a/extensions/contract-termination/build.gradle.kts b/extensions/contract-termination/build.gradle.kts index 61c1ad4ec..a220fc241 100644 --- a/extensions/contract-termination/build.gradle.kts +++ b/extensions/contract-termination/build.gradle.kts @@ -13,8 +13,8 @@ dependencies { implementation(project(":extensions:sovity-messenger")) implementation(libs.edc.coreSpi) - implementation(libs.edc.transferSpi) implementation(libs.edc.dspNegotiationTransform) + implementation(libs.edc.transferSpi) implementation(libs.jakarta.rsApi) diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java index c4e5dc825..1308eaf1c 100644 --- a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java @@ -21,6 +21,7 @@ import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import lombok.val; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; import org.junit.jupiter.api.Test; @@ -34,6 +35,7 @@ @ExtendWith(E2eTestExtension.class) class TerminateContractQueryTest { + @DisabledOnGithub @Test void terminateConsumerAgreementOrThrow_shouldInsertRowInTerminationTable( E2eScenario scenario, diff --git a/extensions/policy-referring-connector/build.gradle.kts b/extensions/policy-referring-connector/build.gradle.kts index c7fa700f5..1df2e5ac9 100644 --- a/extensions/policy-referring-connector/build.gradle.kts +++ b/extensions/policy-referring-connector/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { api(libs.edc.authSpi) api(libs.edc.policyEngineSpi) api(libs.edc.contractSpi) + + testImplementation(libs.edc.junit) testImplementation(libs.mockito.core) diff --git a/extensions/policy-time-interval/build.gradle.kts b/extensions/policy-time-interval/build.gradle.kts index 66b2183c3..a469643c7 100644 --- a/extensions/policy-time-interval/build.gradle.kts +++ b/extensions/policy-time-interval/build.gradle.kts @@ -7,6 +7,8 @@ plugins { dependencies { api(libs.edc.authSpi) api(libs.edc.policyEngineSpi) + + testImplementation(libs.edc.junit) } diff --git a/extensions/postgres-flyway/build.gradle.kts b/extensions/postgres-flyway/build.gradle.kts index 8b0b67dca..96b90d1b5 100644 --- a/extensions/postgres-flyway/build.gradle.kts +++ b/extensions/postgres-flyway/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { // Adds Database-Related EDC-Extensions (EDC-SQL-Stores, JDBC-Driver, Pool and Transactions) implementation(libs.edc.controlPlaneSql) implementation(libs.edc.transactionLocal) + implementation(libs.tractus.sqlPool) implementation(libs.apache.commonsLang) diff --git a/extensions/sovity-messenger/build.gradle.kts b/extensions/sovity-messenger/build.gradle.kts index cd9c7655d..b47765a69 100644 --- a/extensions/sovity-messenger/build.gradle.kts +++ b/extensions/sovity-messenger/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { implementation(libs.edc.managementApiConfiguration) implementation(libs.edc.transformCore) - testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java index f836e5399..d9ef4a47e 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/ContractAgreementPageQuery.java @@ -14,7 +14,6 @@ package de.sovity.edc.ext.wrapper.api.ui.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java index b959ac0ec..e02e50e9c 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/usecase/UseCaseResource.java @@ -19,7 +19,6 @@ import de.sovity.edc.ext.wrapper.api.usecase.model.KpiResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts index fe5a1bf79..909ba8dbe 100644 --- a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -12,11 +12,15 @@ dependencies { api(libs.edc.coreSpi) api(libs.edc.transformCore) api(libs.edc.transformSpi) + api(project(":extensions:wrapper:wrapper-common-api")) api(project(":utils:json-and-jsonld-utils")) + implementation(libs.apache.commonsLang) implementation(libs.apache.commonsCollections) implementation(libs.flexmark.all) + implementation(libs.okhttp.okhttp) + testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PlaceholderEndpointService.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PlaceholderEndpointService.java new file mode 100644 index 000000000..d3c1c955a --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/PlaceholderEndpointService.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import lombok.RequiredArgsConstructor; +import okhttp3.HttpUrl; + +@RequiredArgsConstructor +public class PlaceholderEndpointService { + + private final String baseUrl; + + public static final String DUMMY_ENDPOINT_URL = "/data-source/placeholder/asset"; + + public String getPlaceholderEndpointForAsset(String email, String subject) { + return HttpUrl.parse(baseUrl + DUMMY_ENDPOINT_URL) + .newBuilder() + .addQueryParameter("email", email) + .addQueryParameter("subject", subject) + .build() + .toString(); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java index 32812934d..1c8b3a4bf 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/dataaddress/http/HttpDataSourceMapper.java @@ -14,6 +14,7 @@ package de.sovity.edc.ext.wrapper.api.common.mappers.dataaddress.http; +import de.sovity.edc.ext.wrapper.api.common.mappers.PlaceholderEndpointService; import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceHttpData; import de.sovity.edc.ext.wrapper.api.common.model.UiDataSourceOnRequest; import de.sovity.edc.utils.jsonld.vocab.Prop; @@ -33,6 +34,7 @@ @RequiredArgsConstructor public class HttpDataSourceMapper { private final HttpHeaderMapper httpHeaderMapper; + private final PlaceholderEndpointService placeholderEndpointService; /** * Data Address for type HTTP_DATA @@ -92,8 +94,12 @@ public Map buildOnRequestDataAddress(@NonNull UiDataSourceOnRequ "Need contactPreferredEmailSubject" ); + var placeholderEndpointForAsset = placeholderEndpointService.getPlaceholderEndpointForAsset( + onRequest.getContactEmail(), + onRequest.getContactPreferredEmailSubject()); + var actualDataSource = UiDataSourceHttpData.builder() - .baseUrl("http://0.0.0.0") + .baseUrl(placeholderEndpointForAsset) .build(); var props = buildDataAddress(actualDataSource); diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java index 4c9d7093b..5d03c1a8e 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/Factory.java @@ -50,7 +50,7 @@ public static AssetJsonLdBuilder newAssetJsonLdBuilder(OwnConnectorEndpointServi return new AssetJsonLdBuilder( new DataSourceMapper( new EdcPropertyUtils(), - new HttpDataSourceMapper(new HttpHeaderMapper()) + new HttpDataSourceMapper(new HttpHeaderMapper(), new PlaceholderEndpointService("http://example.com/dummy/baseUrl")) ), newAssetJsonLdParser(ownConnectorEndpointService), new AssetEditRequestMapper() diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java index ea0b8658e..060c29d03 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/asset/AssetJsonLdBuilderTest.java @@ -534,7 +534,7 @@ void test_create_onRequest() { var dataAddress = Json.createObjectBuilder() .add(Prop.TYPE, Prop.Edc.TYPE_DATA_ADDRESS) .add(Prop.Edc.TYPE, Prop.Edc.DATA_ADDRESS_TYPE_HTTP_DATA) - .add(Prop.Edc.BASE_URL, "http://0.0.0.0") + .add(Prop.Edc.BASE_URL, "http://example.com/dummy/baseUrl/data-source/placeholder/asset?email=contact%40example.com&subject=Test") .add(Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY, Prop.SovityDcatExt.DATA_SOURCE_AVAILABILITY_ON_REQUEST) .add(Prop.SovityDcatExt.CONTACT_EMAIL, "contact@example.com") .add(Prop.SovityDcatExt.CONTACT_PREFERRED_EMAIL_SUBJECT, "Test"); diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts index 2a080c73b..027f3c567 100644 --- a/extensions/wrapper/wrapper/build.gradle.kts +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(libs.apache.commonsLang) implementation(libs.edc.apiCore) implementation(libs.edc.managementApiConfiguration) + implementation(libs.edc.dspApiConfiguration) implementation(libs.edc.dspHttpSpi) implementation(libs.jooq.jooq) implementation(libs.hibernate.validation) diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java index 8f2b29210..3c0f81079 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtension.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; import de.sovity.edc.extension.db.directaccess.DslContextFactory; -import de.sovity.edc.extension.messenger.SovityMessenger; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.connector.api.management.configuration.transform.ManagementApiTypeTransformerRegistry; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; @@ -35,7 +34,9 @@ import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.protocol.dsp.api.configuration.DspApiConfiguration; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.CoreConstants; import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.system.ServiceExtension; @@ -45,6 +46,8 @@ public class WrapperExtension implements ServiceExtension { + @Setting(value = "Base URL for the On Request asset datasource, as reachable by the data plane") + public static final String MY_EDC_DATASOURCE_PLACEHOLDER_BASEURL = "my.edc.datasource.placeholder.baseurl"; public static final String EXTENSION_NAME = "WrapperExtension"; @@ -67,14 +70,14 @@ public class WrapperExtension implements ServiceExtension { @Inject private DslContextFactory dslContextFactory; @Inject + private DspApiConfiguration dspApiConfiguration; + @Inject private ManagementApiConfiguration dataManagementApiConfiguration; @Inject private PolicyDefinitionStore policyDefinitionStore; @Inject private PolicyEngine policyEngine; @Inject - private SovityMessenger sovityMessenger; - @Inject private TransferProcessService transferProcessService; @Inject private TransferProcessStore transferProcessStore; @@ -119,7 +122,6 @@ public void initialize(ServiceExtensionContext context) { policyDefinitionService, policyDefinitionStore, policyEngine, - sovityMessenger, transferProcessService, transferProcessStore, typeTransformerRegistry @@ -127,8 +129,11 @@ public void initialize(ServiceExtensionContext context) { wrapperExtensionContext.selfDescriptionService().validateSelfDescriptionConfig(); - wrapperExtensionContext.jaxRsResources().forEach(resource -> + wrapperExtensionContext.managementApiResources().forEach(resource -> webService.registerResource(dataManagementApiConfiguration.getContextAlias(), resource)); + + wrapperExtensionContext.dspApiResources().forEach(resource -> + webService.registerResource(dspApiConfiguration.getContextAlias(), resource)); } private void fixObjectMapperDateSerialization(ObjectMapper objectMapper) { diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java index 7280df3b0..cc135b3bf 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContext.java @@ -22,13 +22,15 @@ /** * Manual Dependency Injection result * - * @param jaxRsResources Jax RS Resource implementations to register. Implementations of + * @param managementApiResources Jax RS Resource implementations to register. Implementations of * APIs supported by our EDC API Client that don't have their own * extension should land here. + * @param dspApiResources Jax RS Resource implementations to register. Rare DSP API * @param selfDescriptionService Required here for validation on start-up */ public record WrapperExtensionContext( - List jaxRsResources, + List managementApiResources, + List dspApiResources, SelfDescriptionService selfDescriptionService ) { } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index d554dd74f..48574717f 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.LegacyPolicyMapper; +import de.sovity.edc.ext.wrapper.api.common.mappers.PlaceholderEndpointService; import de.sovity.edc.ext.wrapper.api.common.mappers.PolicyMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetEditRequestMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.asset.AssetJsonLdBuilder; @@ -75,14 +76,13 @@ import de.sovity.edc.ext.wrapper.api.usecase.pages.catalog.UseCaseCatalogApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.KpiApiService; import de.sovity.edc.ext.wrapper.api.usecase.services.SupportedPolicyApiService; +import de.sovity.edc.ext.wrapper.controller.PlaceholderEndpointController; import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; -import de.sovity.edc.extension.contacttermination.query.ContractAgreementTerminationDetailsQuery; -import de.sovity.edc.extension.contacttermination.query.TerminateContractQuery; import de.sovity.edc.extension.db.directaccess.DslContextFactory; -import de.sovity.edc.extension.messenger.SovityMessenger; import de.sovity.edc.utils.catalog.DspCatalogService; import de.sovity.edc.utils.catalog.mapper.DspDataOfferBuilder; import lombok.NoArgsConstructor; +import lombok.val; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; @@ -135,7 +135,6 @@ public static WrapperExtensionContext buildContext( PolicyDefinitionService policyDefinitionService, PolicyDefinitionStore policyDefinitionStore, PolicyEngine policyEngine, - SovityMessenger sovityMessenger, TransferProcessService transferProcessService, TransferProcessStore transferProcessStore, TypeTransformerRegistry typeTransformerRegistry @@ -148,7 +147,10 @@ public static WrapperExtensionContext buildContext( var edcPropertyUtils = new EdcPropertyUtils(); var selfDescriptionService = new SelfDescriptionService(config, monitor); var ownConnectorEndpointService = new OwnConnectorEndpointServiceImpl(selfDescriptionService); - var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd, ownConnectorEndpointService); + var placeholderEndpointService = new PlaceholderEndpointService( + config.getString(WrapperExtension.MY_EDC_DATASOURCE_PLACEHOLDER_BASEURL, "http://0.0.0.0") + ); + var assetMapper = newAssetMapper(typeTransformerRegistry, jsonLd, ownConnectorEndpointService, placeholderEndpointService); var policyMapper = newPolicyMapper(objectMapper, typeTransformerRegistry, operatorMapper); var transferProcessStateService = new TransferProcessStateService(); var contractNegotiationUtils = new ContractNegotiationUtils( @@ -210,8 +212,6 @@ public static WrapperExtensionContext buildContext( transferRequestBuilder, transferProcessService ); - var agreementDetailsQuery = new ContractAgreementTerminationDetailsQuery(); - var terminateContractQuery = new TerminateContractQuery(); var contractAgreementTerminationApiService = new ContractAgreementTerminationApiService(contractAgreementTerminationService); var legacyPolicyMapper = new LegacyPolicyMapper(); var policyDefinitionApiService = new PolicyDefinitionApiService( @@ -294,19 +294,22 @@ public static WrapperExtensionContext buildContext( supportedPolicyApiService, useCaseCatalogApiService ); + val placeholderEndpointController = new PlaceholderEndpointController(); // Collect all JAX-RS resources - return new WrapperExtensionContext(List.of( - uiResource, - useCaseResource - ), selfDescriptionService); + return new WrapperExtensionContext( + List.of(uiResource, useCaseResource), + List.of(placeholderEndpointController), + selfDescriptionService + ); } @NotNull private static AssetMapper newAssetMapper( TypeTransformerRegistry typeTransformerRegistry, JsonLd jsonLd, - OwnConnectorEndpointService ownConnectorEndpointService + OwnConnectorEndpointService ownConnectorEndpointService, + PlaceholderEndpointService placeholderEndpointService ) { var edcPropertyUtils = new EdcPropertyUtils(); var assetJsonLdUtils = new AssetJsonLdUtils(); @@ -318,7 +321,7 @@ private static AssetMapper newAssetMapper( ownConnectorEndpointService ); var httpHeaderMapper = new HttpHeaderMapper(); - var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper); + var httpDataSourceMapper = new HttpDataSourceMapper(httpHeaderMapper, placeholderEndpointService); var dataSourceMapper = new DataSourceMapper( edcPropertyUtils, httpDataSourceMapper diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java index 6e3411762..47801afaf 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementData.java @@ -21,7 +21,6 @@ import org.eclipse.edc.spi.types.domain.asset.Asset; import java.util.List; -import java.util.Map; /** diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java index 7e35f1a3d..87ba240d3 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java @@ -23,7 +23,6 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementTransferProcess; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminatedBy; import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; -import jakarta.validation.constraints.Null; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; @@ -34,7 +33,6 @@ import java.util.Comparator; import java.util.List; -import java.util.Map; import static de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationStatus.ONGOING; import static de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationStatus.TERMINATED; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/controller/PlaceholderEndpointController.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/controller/PlaceholderEndpointController.java new file mode 100644 index 000000000..a399afa39 --- /dev/null +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/controller/PlaceholderEndpointController.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.controller; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Produces({MediaType.APPLICATION_JSON}) +@Path("/data-source/placeholder") +public class PlaceholderEndpointController { + + @GET + @Path("/{path:.*}") + @Produces(MediaType.TEXT_PLAIN) + @Consumes("*/*") + public Response get(@QueryParam("email") String email, @QueryParam("subject") String subject) { + return Response.ok(""" + This is not real data. + + This asset is accessible on request. + + The offer you are trying to use only has this placeholder as a dummy endpoint and requires you to take extra steps to access it. + + Please contact the data provider for more information about how to access it. + """ + "\n\n" + "Email: " + email + "\n" + "Subject: " + subject + "\n" + ).build(); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa0cdf90c..07d7ab5ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -121,6 +121,7 @@ edc-policyDefinitionApi = { module = "org.eclipse.edc:policy-definition-api", ve edc-policyEngineSpi = { module = "org.eclipse.edc:policy-engine-spi", version.ref = "edc" } edc-policyModel = { module = "org.eclipse.edc:policy-model", version.ref = "edc" } edc-policySpi = { module = "org.eclipse.edc:policy-spi", version.ref = "edc" } +edc-runtimeMetamodel = { module = "org.eclipse.edc:runtime-metamodel", version = "0.2.1" } edc-sqlCore = { module = "org.eclipse.edc:sql-core", version.ref = "edc" } edc-transactionLocal = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-transferDataPlane = { module = "org.eclipse.edc:transfer-data-plane", version.ref = "edc" } diff --git a/launchers/.env.connector b/launchers/.env.connector index d58c1267a..5b2eb4284 100644 --- a/launchers/.env.connector +++ b/launchers/.env.connector @@ -51,6 +51,7 @@ EDC_DSP_CALLBACK_ADDRESS=${MY_EDC_PROTOCOL}${MY_EDC_FQDN}${WEB_HTTP_PROTOCOL_PAT EDC_UI_MANAGEMENT_API_URL_SHOWN_IN_DASHBOARD=${MY_EDC_PROTOCOL}${MY_EDC_FQDN}${WEB_HTTP_MANAGEMENT_PATH} # Flyway Extension: Defaults +EDC_SERVER_DB_CONNECTION_POOL_SIZE=10 EDC_DATASOURCE_DEFAULT_NAME=default EDC_DATASOURCE_DEFAULT_URL=$MY_EDC_JDBC_URL EDC_DATASOURCE_DEFAULT_USER=$MY_EDC_JDBC_USER @@ -95,3 +96,6 @@ EDC_AGENT_IDENTITY_KEY=referringConnector # but for some reason it is required, and EDC won't start up if it isn't configured # it is created in the Dockerfile EDC_VAULT=/app/empty-properties-file.properties + +# Base URL for the On Request asset datasource, as reachable by the data plane +MY_EDC_DATASOURCE_PLACEHOLDER_BASEURL=${EDC_DSP_CALLBACK_ADDRESS} diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 8b232a5f1..e5e0d221b 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { testImplementation(libs.junit.api) testImplementation(libs.junit.params) testImplementation(libs.mockserver.netty) + testImplementation(libs.okhttp.okhttp) testImplementation(libs.restAssured.restAssured) testRuntimeOnly(libs.junit.engine) } diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index a20f44b0c..7b39169a7 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -137,6 +137,7 @@ void canGetAgreementPageForTerminatedContract( assertTermination(agreementsAfterTermination, details, reason, SELF); } + @DisabledOnGithub @Test @SneakyThrows void canTerminateFromConsumer( @@ -253,6 +254,7 @@ void limitTheDetailSizeAt1000Chars( // termination completed == success } + @DisabledOnGithub @TestFactory List theDetailsAreMandatory( E2eScenario scenario, @@ -291,6 +293,7 @@ List theDetailsAreMandatory( })).toList(); } + @DisabledOnGithub @Test @SneakyThrows void canTerminateFromProvider( diff --git a/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java b/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java new file mode 100644 index 000000000..5e6fb8b7f --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.DataSourceType; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.UiAssetCreateRequest; +import de.sovity.edc.client.gen.model.UiDataSource; +import de.sovity.edc.client.gen.model.UiDataSourceOnRequest; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import jakarta.ws.rs.HttpMethod; +import lombok.SneakyThrows; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.HttpStatusCode; + +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; + +@ExtendWith(E2eTestExtension.class) +class PlaceholderDataSourceExtensionTest { + + @SneakyThrows + @Test + void shouldAccessDummyEndpoint( + E2eScenario scenario, + ClientAndServer clientAndServer, + @Provider EdcClient providerClient + ) { + // arrange + + val assetId = "asset-1"; + val email = "contact@example.com"; + val subject = "Data request"; + providerClient.uiApi().createAsset( + UiAssetCreateRequest.builder() + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail(email) + .contactPreferredEmailSubject(subject) + .build()) + .build()) + .id(assetId) + .build() + ); + + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + val accessed = new AtomicReference<>("Not accessed."); + val destinationPath = "/foo/bar"; + val destinationUrl = "http://localhost:" + clientAndServer.getPort() + destinationPath; + + clientAndServer.when(HttpRequest.request().withMethod(HttpMethod.POST)) + .respond((it) -> { + accessed.set(it.getBodyAsString()); + return HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()); + }); + + scenario.transferAndAwait(InitiateTransferRequest.builder() + .contractAgreementId(negotiation.getContractAgreementId()) + .dataSinkProperties(Map.of( + EDC_NAMESPACE + "baseUrl", destinationUrl, + EDC_NAMESPACE + "method", HttpMethod.POST, + EDC_NAMESPACE + "type", "HttpData" + )) + .build()); + + // assert + assertThat(new String(Base64.getDecoder().decode(accessed.get()))) + .contains("This is not real data.") + .contains(email) + .contains(subject); + } +} diff --git a/utils/catalog-parser/build.gradle.kts b/utils/catalog-parser/build.gradle.kts index dee5ebaf0..f5d8fc433 100644 --- a/utils/catalog-parser/build.gradle.kts +++ b/utils/catalog-parser/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(libs.apache.commonsCollections) implementation(libs.apache.commonsIo) + testAnnotationProcessor(libs.lombok) testCompileOnly(libs.lombok) testImplementation(project(":utils:test-utils")) diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index e2d6e9038..b6b1eab88 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { api(libs.edc.junit) api(libs.awaitility.java) + api(libs.postgres) api(project(":extensions:wrapper:clients:java-client")) api(project(":utils:json-and-jsonld-utils")) diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java index 4608b9396..b2fab5ea9 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java @@ -85,8 +85,6 @@ public static ConnectorConfig basicEdcConfig(String participantId, int firstPort properties.put("edc.last.commit.info", "test env commit message"); properties.put("edc.build.date", "2023-05-08T15:30:00Z"); - properties.put("edc.server.db.connection.timeout.in.ms", "5000"); - properties.put("my.edc.participant.id", participantId); properties.put("my.edc.title", "Connector Title %s".formatted(participantId)); properties.put("my.edc.description", "Connector Description %s".formatted(participantId)); @@ -95,7 +93,7 @@ public static ConnectorConfig basicEdcConfig(String participantId, int firstPort properties.put("my.edc.maintainer.url", "http://maintainer.%s".formatted(participantId)); properties.put("my.edc.maintainer.name", "Maintainer Name %s".formatted(participantId)); - properties.put("edc.server.db.connection.pool.size", "3"); + properties.put("my.edc.datasource.placeholder.baseurl", apiConfig.getProtocolApiGroup().getUri().toString()); properties.put("web.http.port", String.valueOf(apiConfig.getDefaultApiGroup().port())); properties.put("web.http.path", String.valueOf(apiConfig.getDefaultApiGroup().path())); diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java index 868c3d89d..eab4e8eea 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionWithTestDatabase.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import lombok.val; +import org.eclipse.edc.junit.extensions.EdcExtension; import org.jooq.DSLContext; import org.jooq.impl.DSL; import org.junit.jupiter.api.extension.AfterAllCallback; @@ -47,16 +48,28 @@ public void beforeAll(ExtensionContext extensionContext) throws Exception { @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - boolean isJooqDsl = parameterContext.getParameter().getType().equals(DSLContext.class); - return isJooqDsl || edcRuntimeExtension.supportsParameter(parameterContext, extensionContext); + + val type = parameterContext.getParameter().getType(); + + if (DSLContext.class.equals(type)) { + return true; + } else if (EdcExtension.class.equals(type)) { + return true; + } + + return edcRuntimeExtension.supportsParameter(parameterContext, extensionContext); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - boolean isJooqDsl = parameterContext.getParameter().getType().equals(DSLContext.class); - if (isJooqDsl) { + + val type = parameterContext.getParameter().getType(); + + if (DSLContext.class.equals(type)) { return getDslContext().dsl(); + } else if (EdcExtension.class.equals(type)) { + return edcRuntimeExtension; } else { return edcRuntimeExtension.resolveParameter(parameterContext, extensionContext); } diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java index 346e256e6..a8560544e 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java @@ -43,13 +43,13 @@ import lombok.val; import org.awaitility.Awaitility; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.jetbrains.annotations.NotNull; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import java.time.Duration; import java.time.OffsetDateTime; -import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -96,7 +96,11 @@ public String createAsset() { .build()) .build(); - return internalCreateAsset("asset-" + assetCounter.getAndIncrement(), dummyDataSource).getId(); + return internalCreateAsset(nextAssetId(), dummyDataSource).getId(); + } + + private @NotNull String nextAssetId() { + return "asset-" + assetCounter.getAndIncrement(); } public String createAsset(String id, UiDataSourceHttpData uiDataSourceHttpData) { @@ -108,6 +112,10 @@ public String createAsset(String id, UiDataSourceHttpData uiDataSourceHttpData) return internalCreateAsset(id, uiDataSource).getId(); } + public String createAsset(String id, UiDataSource uiDataSource) { + return internalCreateAsset(id, uiDataSource).getId(); + } + public MockedAsset createAssetWithMockResource(String id) { val path = "/assets/" + id; @@ -141,31 +149,8 @@ private IdResponseDto internalCreateAsset(String assetId, UiDataSource dataSourc .build()); } - public String createPolicyDefinition(String policyId, UiPolicyConstraint... constraints) { - return createPolicyDefinition(policyId, Arrays.stream(constraints).toList()).getId(); - } - - private IdResponseDto createPolicyDefinition(String policyId, List constraints) { - var expression = UiPolicyExpression.builder() - .type(UiPolicyExpressionType.AND) - .expressions(constraints.stream() - .map(it -> UiPolicyExpression.builder() - .type(UiPolicyExpressionType.CONSTRAINT) - .constraint(it) - .build()) - .toList()) - .build(); - - var policyDefinition = PolicyDefinitionCreateDto.builder() - .policyDefinitionId(policyId) - .expression(expression) - .build(); - - return providerClient.uiApi().createPolicyDefinitionV2(policyDefinition); - } - public String createContractDefinition(String assetId) { - return createContractDefinition(POLICY_DEFINITION_ID, assetId).getId(); + return createContractDefinition(alwaysTruePolicyId, assetId).getId(); } public IdResponseDto createContractDefinition(String policyId, String assetId) { diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java index ddbd592ae..95f45ec4b 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java @@ -50,7 +50,8 @@ public class E2eTestExtension private ConnectorConfig providerConfig; private final EdcRuntimeExtensionWithTestDatabase providerExtension; - private final List> partySupportedTypes = List.of(ConnectorConfig.class, EdcClient.class, ConnectorRemote.class, ClientAndServer.class); + private final List> partySupportedTypes = + List.of(ConnectorConfig.class, EdcClient.class, ConnectorRemote.class, ClientAndServer.class); private final List> supportedTypes = Stream.concat(partySupportedTypes.stream(), Stream.of(E2eScenario.class)).toList(); private Lazy clientAndServer; diff --git a/utils/versions/build.gradle.kts b/utils/versions/build.gradle.kts index 2d27f8408..6d9aca986 100644 --- a/utils/versions/build.gradle.kts +++ b/utils/versions/build.gradle.kts @@ -5,8 +5,6 @@ import com.squareup.javapoet.TypeSpec import javax.lang.model.element.Modifier.FINAL import javax.lang.model.element.Modifier.PUBLIC import javax.lang.model.element.Modifier.STATIC -import java.lang.String as JavaString - plugins { `java-library` @@ -26,7 +24,7 @@ val generateGradleVersions by tasks.creating { val versionsClass = TypeSpec.classBuilder("GradleVersions") .addModifiers(PUBLIC, FINAL) .addField( - FieldSpec.builder(TypeName.get(JavaString::class.java), "POSTGRES_IMAGE_TAG") + FieldSpec.builder(TypeName.get(String::class.java), "POSTGRES_IMAGE_TAG") .initializer("\$S", libs.versions.postgresDbImage.get()) .addModifiers(PUBLIC, STATIC, FINAL) .build() From 15f2e6ee38b0e76edc4afd27ab287c7ae3fa2153 Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:22:54 +0200 Subject: [PATCH 269/295] feat: rework always-true policy definition (#1005) Co-authored-by: Richard Treier --- .env | 6 +- CHANGELOG.md | 2 + docker-compose.yaml | 2 + .../AlwaysTruePolicyDefinitionService.java | 25 +- .../V11__migrate_always_true_policy.sql | 19 ++ .../AlwaysTrueMigrationNewConsumerTest.java | 57 ++++ .../AlwaysTrueMigrationNewProviderTest.java | 58 ++++ .../de/sovity/edc/e2e/ApiWrapperDemoTest.java | 2 + .../edc/e2e/ContractTerminationTest.java | 8 +- .../AlwaysTruePolicyMigrationCommonTest.java | 51 +++ ...1_1__test_always_true_policy_migration.sql | 35 +++ ...0_1__test_always_true_policy_migration.sql | 35 +++ .../V1_9__ms8-test-contract-consumer.sql | 67 ---- .../V1_9__ms8-test-contract-provider.sql | 292 ------------------ utils/test-utils/build.gradle.kts | 1 + .../e2e/connector/DataTransferTestUtil.java | 40 +-- .../extension/e2e/extension/E2eScenario.java | 5 +- .../e2e/extension/E2eTestExtension.java | 24 +- 18 files changed, 319 insertions(+), 410 deletions(-) create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V11__migrate_always_true_policy.sql create mode 100644 tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java create mode 100644 tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java create mode 100644 tests/src/test/resources/db/additional-test-data/always-true-policy-legacy/V11_1__test_always_true_policy_migration.sql create mode 100644 tests/src/test/resources/db/additional-test-data/always-true-policy-migrated/V10_1__test_always_true_policy_migration.sql delete mode 100644 tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql delete mode 100644 tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql diff --git a/.env b/.env index 1d0ed94d1..ef3c40517 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:9.0.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:9.0.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.0.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:10.0.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.0.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.0 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/CHANGELOG.md b/CHANGELOG.md index b04aaaea8..cd863ca60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). - Both providers and consumers can now terminate their contracts. - Added endpoints for checking ID availability for policies, assets and contract definitions +- The always-true policy is now created with no constraints instead of the artificial `ALWAYS_TRUE = TRUE` constraint + - Existing always-true policy definitions are migrated to the new format - existing contract agreements are not affected #### Patch Changes diff --git a/docker-compose.yaml b/docker-compose.yaml index 01d93ec40..8dfd7e1af 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -42,6 +42,7 @@ services: EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' EDC_WEB_REST_CORS_ORIGINS: '*' EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: - '11001:11001' - '11002:11002' @@ -89,6 +90,7 @@ services: EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,X-Api-Key' EDC_WEB_REST_CORS_ORIGINS: '*' EDC_AGENT_IDENTITY_KEY: 'client_id' # required for Mock IAM to work + ports: - '22001:11001' - '22002:11002' diff --git a/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java index 101b7364e..be0b26f0c 100644 --- a/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java +++ b/extensions/policy-always-true/src/main/java/de/sovity/edc/extension/policy/services/AlwaysTruePolicyDefinitionService.java @@ -17,14 +17,9 @@ import org.eclipse.edc.connector.policy.spi.PolicyDefinition; import org.eclipse.edc.connector.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.policy.model.Action; -import org.eclipse.edc.policy.model.AtomicConstraint; -import org.eclipse.edc.policy.model.LiteralExpression; -import org.eclipse.edc.policy.model.Operator; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.policy.model.Policy; -import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE; -import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.EXPRESSION_RIGHT_VALUE; import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; /** @@ -50,22 +45,16 @@ public boolean exists() { * Creates policy definition "always-true". */ public void create() { - var alwaysTrueConstraint = AtomicConstraint.Builder.newInstance() - .leftExpression(new LiteralExpression(EXPRESSION_LEFT_VALUE)) - .operator(Operator.EQ) - .rightExpression(new LiteralExpression(EXPRESSION_RIGHT_VALUE)) - .build(); var alwaysTruePermission = Permission.Builder.newInstance() - .action(Action.Builder.newInstance().type("USE").build()) - .constraint(alwaysTrueConstraint) - .build(); + .action(Action.Builder.newInstance().type("USE").build()) + .build(); var policy = Policy.Builder.newInstance() - .permission(alwaysTruePermission) - .build(); + .permission(alwaysTruePermission) + .build(); var policyDefinition = PolicyDefinition.Builder.newInstance() - .id(POLICY_DEFINITION_ID) - .policy(policy) - .build(); + .id(POLICY_DEFINITION_ID) + .policy(policy) + .build(); policyDefinitionService.create(policyDefinition); } } diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V11__migrate_always_true_policy.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V11__migrate_always_true_policy.sql new file mode 100644 index 000000000..1b24d14ea --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V11__migrate_always_true_policy.sql @@ -0,0 +1,19 @@ +-- +-- Copyright (c) 2024 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - initial API and implementation +-- + +update edc_policydefinitions +set permissions = jsonb_set( + permissions::jsonb, + '{0,constraints}', + '[]'::jsonb)::json +where policy_id = 'always-true'; diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java new file mode 100644 index 000000000..69e206892 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java @@ -0,0 +1,57 @@ +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; +import de.sovity.edc.e2e.common.AlwaysTruePolicyMigrationCommonTest; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockserver.integration.ClientAndServer; + +import static org.assertj.core.api.Assertions.assertThat; + +class AlwaysTrueMigrationNewConsumerTest { + + @RegisterExtension + private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() + .additionalConsumerMigrationLocation("classpath:db/additional-test-data/always-true-policy-migrated") + .additionalProviderMigrationLocation("classpath:db/additional-test-data/always-true-policy-legacy") + .build(); + + @Test + @DisabledOnGithub + void always_true_agreements_still_work_after_migration( + E2eScenario scenario, + ClientAndServer mockServer, + @Provider EdcClient providerClient, + @Consumer EdcClient consumerClient + ) { + // assert correct policies + val oldAlwaysTruePolicyCreatedAfterMigration = providerClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter( + policy -> policy.getPolicyDefinitionId().equals(AlwaysTruePolicyConstants.POLICY_DEFINITION_ID) + ).findFirst().orElseThrow().getPolicy().getExpression(); + + val migratedAlwaysTruePolicy = consumerClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter( + policy -> policy.getPolicyDefinitionId().equals(AlwaysTruePolicyConstants.POLICY_DEFINITION_ID) + ).findFirst().orElseThrow().getPolicy().getExpression(); + + assertThat(oldAlwaysTruePolicyCreatedAfterMigration.getType()).isEqualTo(UiPolicyExpressionType.CONSTRAINT); + assertThat(oldAlwaysTruePolicyCreatedAfterMigration.getConstraint().getLeft()).isEqualTo( + AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE); + assertThat(oldAlwaysTruePolicyCreatedAfterMigration.getConstraint().getRight().getValue()).isEqualTo( + AlwaysTruePolicyConstants.EXPRESSION_RIGHT_VALUE); + assertThat(oldAlwaysTruePolicyCreatedAfterMigration.getConstraint().getOperator()).isEqualTo(OperatorDto.EQ); + + assertThat(migratedAlwaysTruePolicy.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); + assertThat(migratedAlwaysTruePolicy.getConstraint()).isNull(); + + AlwaysTruePolicyMigrationCommonTest.alwaysTruePolicyMigrationTest(scenario, mockServer, providerClient, consumerClient); + } +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java new file mode 100644 index 000000000..bd2ea7b6b --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java @@ -0,0 +1,58 @@ +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.UiPolicyExpressionType; +import de.sovity.edc.e2e.common.AlwaysTruePolicyMigrationCommonTest; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockserver.integration.ClientAndServer; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class AlwaysTrueMigrationNewProviderTest { + + @RegisterExtension + private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() + .additionalConsumerMigrationLocation("classpath:db/additional-test-data/alwaysTruePolicyMigrationTest") + .additionalProviderMigrationLocation("") + .build(); + + @Test + @DisabledOnGithub + void always_true_agreements_still_work_after_migration( + E2eScenario scenario, + ClientAndServer mockServer, + @Provider EdcClient providerClient, + @Consumer EdcClient consumerClient + ) { + // assert correct policies + val newAlwaysTruePolicyOnProvider = providerClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter( + policy -> policy.getPolicyDefinitionId().equals(AlwaysTruePolicyConstants.POLICY_DEFINITION_ID) + ).findFirst().orElseThrow().getPolicy().getExpression(); + + val oldAlwaysTruePolicyOnConsumer = consumerClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter( + policy -> policy.getPolicyDefinitionId().equals(AlwaysTruePolicyConstants.POLICY_DEFINITION_ID) + ).findFirst().orElseThrow().getPolicy().getExpression(); + + assertThat(oldAlwaysTruePolicyOnConsumer.getType()).isEqualTo(UiPolicyExpressionType.CONSTRAINT); + assertThat(oldAlwaysTruePolicyOnConsumer.getConstraint().getLeft()).isEqualTo(AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE); + assertThat(oldAlwaysTruePolicyOnConsumer.getConstraint().getRight().getValue()).isEqualTo( + AlwaysTruePolicyConstants.EXPRESSION_RIGHT_VALUE); + assertThat(oldAlwaysTruePolicyOnConsumer.getConstraint().getOperator()).isEqualTo(OperatorDto.EQ); + + assertThat(newAlwaysTruePolicyOnProvider.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); + assertThat(newAlwaysTruePolicyOnProvider.getConstraint()).isNull(); + + AlwaysTruePolicyMigrationCommonTest.alwaysTruePolicyMigrationTest(scenario, mockServer, providerClient, consumerClient); + } + + +} diff --git a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java index 71143851d..88c0e5f63 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ApiWrapperDemoTest.java @@ -41,6 +41,7 @@ import de.sovity.edc.extension.e2e.connector.MockDataAddressRemote; import de.sovity.edc.extension.e2e.db.TestDatabase; import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.jsonld.vocab.Prop; import org.awaitility.Awaitility; import org.eclipse.edc.junit.extensions.EdcExtension; @@ -108,6 +109,7 @@ void setup() { } @Test + @DisabledOnGithub void provide_and_consume() { // provider: create data offer createPolicy(); diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index 7b39169a7..4b71abef4 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -27,6 +27,7 @@ import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.ws.rs.HttpMethod; import lombok.SneakyThrows; import lombok.val; @@ -61,6 +62,7 @@ public class ContractTerminationTest { @Test + @DisabledOnGithub void canGetAgreementPageForNonTerminatedContract( E2eScenario scenario, @Consumer EdcClient consumerClient, @@ -364,9 +366,9 @@ void cantTransferDataAfterTerminated( .contractAgreementId(negotiation.getContractAgreementId()) .dataSinkProperties( Map.of( - EDC_NAMESPACE + "baseUrl", destinationUrl, - EDC_NAMESPACE + "method", HttpMethod.POST, - EDC_NAMESPACE + "type", "HttpData" + Prop.Edc.BASE_URL, destinationUrl, + Prop.Edc.METHOD, HttpMethod.POST, + Prop.Edc.TYPE, "HttpData" ) ) .build(); diff --git a/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java b/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java new file mode 100644 index 000000000..46d487d6c --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java @@ -0,0 +1,51 @@ +package de.sovity.edc.e2e.common; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; +import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.ws.rs.HttpMethod; +import lombok.val; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; + +public class AlwaysTruePolicyMigrationCommonTest { + + public static void alwaysTruePolicyMigrationTest(E2eScenario scenario, ClientAndServer mockServer, EdcClient providerClient, EdcClient consumerClient) { + // arrange + val destinationPath = "/destination/some/path/"; + val destinationUrl = "http://localhost:" + mockServer.getPort() + destinationPath; + mockServer.when(HttpRequest.request(destinationPath).withMethod("POST")).respond(it -> HttpResponse.response().withStatusCode(200)); + + val asset = scenario.createAsset(); + scenario.createContractDefinition(asset); //this automatically uses the always-true policy + val negotiation = scenario.negotiateAssetAndAwait(asset); + + // act + val transfer = scenario.transferAndAwait( + InitiateTransferRequest.builder() + .contractAgreementId(negotiation.getContractAgreementId()) + .dataSinkProperties( + Map.of( + Prop.Edc.BASE_URL, destinationUrl, + Prop.Edc.METHOD, HttpMethod.POST, + Prop.Edc.TYPE, "HttpData" + ) + ) + .build() + ); + val transferProcess = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().stream().filter( + process -> process.getTransferProcessId().equals(transfer) + ).findFirst().orElseThrow(); + + // assert + assertThat(transferProcess.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); + } +} diff --git a/tests/src/test/resources/db/additional-test-data/always-true-policy-legacy/V11_1__test_always_true_policy_migration.sql b/tests/src/test/resources/db/additional-test-data/always-true-policy-legacy/V11_1__test_always_true_policy_migration.sql new file mode 100644 index 000000000..6d0ba3856 --- /dev/null +++ b/tests/src/test/resources/db/additional-test-data/always-true-policy-legacy/V11_1__test_always_true_policy_migration.sql @@ -0,0 +1,35 @@ +insert into edc.public.edc_policydefinitions +(policy_id, permissions, prohibitions, duties, extensible_properties, policy_type, created_at) +values ('always-true', '[ + { + "edctype": "dataspaceconnector:permission", + "target": null, + "action": { + "type": "USE", + "includedIn": null, + "constraint": null + }, + "assignee": null, + "assigner": null, + "constraints": [ + { + "edctype": "AtomicConstraint", + "leftExpression": { + "edctype": "dataspaceconnector:literalexpression", + "value": "ALWAYS_TRUE" + }, + "rightExpression": { + "edctype": "dataspaceconnector:literalexpression", + "value": "true" + }, + "operator": "EQ" + } + ], + "duties": [] + } +]', + '[]', + '[]', + '{}', + '{"@policytype":"set"}', + 1721740661) diff --git a/tests/src/test/resources/db/additional-test-data/always-true-policy-migrated/V10_1__test_always_true_policy_migration.sql b/tests/src/test/resources/db/additional-test-data/always-true-policy-migrated/V10_1__test_always_true_policy_migration.sql new file mode 100644 index 000000000..6d0ba3856 --- /dev/null +++ b/tests/src/test/resources/db/additional-test-data/always-true-policy-migrated/V10_1__test_always_true_policy_migration.sql @@ -0,0 +1,35 @@ +insert into edc.public.edc_policydefinitions +(policy_id, permissions, prohibitions, duties, extensible_properties, policy_type, created_at) +values ('always-true', '[ + { + "edctype": "dataspaceconnector:permission", + "target": null, + "action": { + "type": "USE", + "includedIn": null, + "constraint": null + }, + "assignee": null, + "assigner": null, + "constraints": [ + { + "edctype": "AtomicConstraint", + "leftExpression": { + "edctype": "dataspaceconnector:literalexpression", + "value": "ALWAYS_TRUE" + }, + "rightExpression": { + "edctype": "dataspaceconnector:literalexpression", + "value": "true" + }, + "operator": "EQ" + } + ], + "duties": [] + } +]', + '[]', + '[]', + '{}', + '{"@policytype":"set"}', + 1721740661) diff --git a/tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql b/tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql deleted file mode 100644 index 30929f8bd..000000000 --- a/tests/src/test/resources/db/additional-test-data/consumer/V1_9__ms8-test-contract-consumer.sql +++ /dev/null @@ -1,67 +0,0 @@ --- --- Data for Name: edc_asset; Type: TABLE DATA; Schema: public; Owner: edc --- - - - --- --- Data for Name: edc_asset_dataaddress; Type: TABLE DATA; Schema: public; Owner: edc --- - - - --- --- Data for Name: edc_asset_property; Type: TABLE DATA; Schema: public; Owner: edc --- - - - --- --- Data for Name: edc_contract_agreement; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_contract_agreement VALUES ('first-cd:28356d13-7fac-4540-80f2-22972c975ecb', 'urn:connector:provider', 'urn:connector:consumer', 1695207988, 1695207986, 1726743986, 'urn:artifact:first-asset:1.0', '{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}}'); - - --- --- Data for Name: edc_contract_definitions; Type: TABLE DATA; Schema: public; Owner: edc --- - - - --- --- Data for Name: edc_contract_negotiation; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_contract_negotiation VALUES ('793d9064-8466-466e-93d1-25c379942c0d', NULL, 'urn:connector:example-provider', 'http://localhost:' || - '21003/api/v1/ids/data', 'ids-multipart', 0, 1200, 1, 1695207988404, NULL, 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '[{"id":"first-cd:95c164c0-2bdd-4c1e-82e2-bec36e0664a5","policy":{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}},"asset":{"id":"urn:artifact:first-asset:1.0","createdAt":1695207986324,"properties":{"asset:prop:id":"urn:artifact:first-asset:1.0"}},"provider":"urn:connector:provider","consumer":"urn:connector:consumer","offerStart":null,"offerEnd":null,"contractStart":"2023-09-20T11:06:26.32476749Z","contractEnd":"2024-09-19T11:06:26.32476749Z"}]', '{}', NULL, 1695207986331, 1695207988404); - - --- --- Data for Name: edc_data_plane_instance; Type: TABLE DATA; Schema: public; Owner: edc --- - --- --- Data for Name: edc_lease; Type: TABLE DATA; Schema: public; Owner: edc --- - - - --- --- Data for Name: edc_policydefinitions; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_policydefinitions VALUES ('always-true', '[{"edctype":"dataspaceconnector:permission","uid":null,"target":null,"action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"ALWAYS_TRUE"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"true"},"operator":"EQ"}],"duties":[]}]', '[]', '[]', '{}', NULL, NULL, NULL, NULL, '{"@policytype":"set"}', 1695137823418); - - --- --- Data for Name: edc_transfer_process; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_transfer_process VALUES ('946aadd4-d4bf-47e9-8aea-c2279070e839', 'CONSUMER', 800, 1, 1695208011094, 1695208008652, '{}', NULL, '{"definitions":[]}', NULL, NULL, '[]', NULL, 1695208011094, '{}'); - --- --- Data for Name: edc_data_request; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_data_request VALUES ('f9a60bc8-0cb5-4f30-8604-2e3b1d020541', '946aadd4-d4bf-47e9-8aea-c2279070e839', 'http://localhost:21003/api/v1/ids/data', 'ids-multipart', 'consumer', 'urn:artifact:first-asset:1.0', 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '{"properties":{"baseUrl":"https://webhook.site/6d5008a7-8c29-4e14-83c1-cc64f86d5398","method":"POST","type":"HttpData"}}', false, '{}', '{"contentType":"application/octet-stream","isFinite":true}', '946aadd4-d4bf-47e9-8aea-c2279070e839'); diff --git a/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql b/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql deleted file mode 100644 index 0d40dfbbd..000000000 --- a/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql +++ /dev/null @@ -1,292 +0,0 @@ --- --- Data for Name: edc_asset; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_asset VALUES ('urn:artifact:first-asset:1.0', 1695207769374); -INSERT INTO public.edc_asset VALUES ('urn:artifact:second-asset', 1695207798635); - - --- --- Data for Name: edc_asset_dataaddress; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_asset_dataaddress VALUES ('urn:artifact:first-asset:1.0', '{"baseUrl":"http://localhost:23001/api/test-backend/data-source","method":"GET","queryParams":"data=first-asset-data","type":"HttpData"}'); -INSERT INTO public.edc_asset_dataaddress VALUES ('urn:artifact:second-asset', '{"baseUrl":"http://localhost:23001/api/test-backend/data-source","method":"GET","queryParams":"data=second-asset-data","type":"HttpData"}'); - - --- --- Data for Name: edc_asset_property; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:curatorOrganizationName', 'Example GmbH', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:originatorOrganization', 'Example GmbH', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#transportMode', 'Rail', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:contenttype', 'text/plain', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyMethod', 'false', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:version', '1.0', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#geoReferenceMethod', 'geo-ref', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:id', 'urn:artifact:first-asset:1.0', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#dataModel', 'data-model', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#dataSubcategory', 'Accidents', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyPath', 'false', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyQueryParams', 'false', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:language', 'https://w3id.org/idsa/code/EN', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:keywords', 'first, asset', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:name', 'First Asset', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:description', 'My First Asset', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:datasource:http:hints:proxyBody', 'false', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:endpointDocumentation', 'https://endpoint-documentation', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:publisher', 'https://publisher', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://w3id.org/mds#dataCategory', 'Traffic Information', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:originator', 'http://localhost:21003/api/v1/ids/data', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:standardLicense', 'https://standard-license', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:usecase', 'my-use-case', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:second-asset', 'asset:prop:id', 'urn:artifact:second-asset', 'java.lang.String'); - - --- --- Data for Name: edc_contract_agreement; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_contract_agreement VALUES ('first-cd:28356d13-7fac-4540-80f2-22972c975ecb', 'urn:connector:provider', 'urn:connector:consumer', 1695207988, 1695207986, 1726743986, 'urn:artifact:first-asset:1.0', '{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}}'); - - --- --- Data for Name: edc_contract_definitions; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_contract_definitions VALUES ('first-cd', 'first-policy', 'first-policy', '{"criteria":[{"operandLeft":"asset:prop:id","operator":"in","operandRight":["urn:artifact:first-asset:1.0"]}]}', 1695207936442, 31536000); -INSERT INTO public.edc_contract_definitions VALUES ('second-cd', 'always-true', 'always-true', '{"criteria":[{"operandLeft":"asset:prop:id","operator":"in","operandRight":["urn:artifact:second-asset"]}]}', 1695207948854, 31536000); - - --- --- Data for Name: edc_contract_negotiation; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_contract_negotiation VALUES ('34ad04cd-4ce0-485f-a12e-aee0e37a9f03', '793d9064-8466-466e-93d1-25c379942c0d', 'urn:connector:example-connector', 'http://localhost:23003/api/v1/ids/data', 'ids-multipart', 1, 1200, 1, 1695207988502, NULL, 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '[{"id":"first-cd:95c164c0-2bdd-4c1e-82e2-bec36e0664a5","policy":{"permissions":[{"edctype":"dataspaceconnector:permission","uid":null,"target":"urn:artifact:first-asset:1.0","action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}],"prohibitions":[],"obligations":[],"extensibleProperties":{},"inheritsFrom":null,"assigner":null,"assignee":null,"target":"urn:artifact:first-asset:1.0","@type":{"@policytype":"set"}},"asset":{"id":"urn:artifact:first-asset:1.0","createdAt":1695207769374,"properties":{"asset:prop:curatorOrganizationName":"Example GmbH","http://w3id.org/mds#transportMode":"Rail","asset:prop:contenttype":"text/plain","asset:prop:datasource:http:hints:proxyMethod":"false","asset:prop:version":"1.0","http://w3id.org/mds#geoReferenceMethod":"geo-ref","asset:prop:id":"urn:artifact:first-asset:1.0","http://w3id.org/mds#dataModel":"data-model","http://w3id.org/mds#dataSubcategory":"Accidents","asset:prop:datasource:http:hints:proxyPath":"false","asset:prop:datasource:http:hints:proxyQueryParams":"false","asset:prop:language":"https://w3id.org/idsa/code/EN","asset:prop:keywords":"first, asset","asset:prop:name":"first-asset","asset:prop:description":"My First Asset","asset:prop:datasource:http:hints:proxyBody":"false","asset:prop:endpointDocumentation":"https://endpoint-documentation","asset:prop:publisher":"https://publisher","http://w3id.org/mds#dataCategory":"Traffic Information","asset:prop:originator":"http://localhost:21003/api/v1/ids/data","asset:prop:standardLicense":"https://standard-license"}},"provider":"urn:connector:provider","consumer":"urn:connector:consumer","offerStart":null,"offerEnd":null,"contractStart":"2023-09-20T11:06:26.324Z","contractEnd":"2024-09-19T11:06:26.324Z"}]', '{}', NULL, 1695207987357, 1695207988502); - --- --- Data for Name: edc_data_plane_instance; Type: TABLE DATA; Schema: public; Owner: edc --- - --- --- Data for Name: edc_lease; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208010863, 60000, '70080388-d8d0-4a0f-b22d-6dec3f9ec10b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208071147, 60000, '1c6b5845-6d0a-481b-b7b3-9821cce8dbe4'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208131518, 60000, 'e919d926-86b6-4adf-82d2-000ff4680d4a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208191767, 60000, 'bd7b7d52-860c-4411-840f-7d30ccd6ea82'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208252016, 60000, 'a41aac77-9573-41c3-b320-67d4e1211548'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208312268, 60000, 'ae7a5a2a-7032-4032-8852-a33f1dfbf564'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208372514, 60000, '72dfc775-d08b-4953-a12c-5ed2b6cb9969'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208432761, 60000, 'b7c53380-3f48-472b-baeb-4cb4c8eaa8d4'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208492995, 60000, 'bd5f7e8d-598e-4ef5-9419-dbc2247fb4f1'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208553244, 60000, '19c3ad84-ca37-49c4-a6b0-86c92f7eb0c3'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208613485, 60000, '757191a2-956a-4057-b149-1ab567924b90'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208673732, 60000, '79d3685c-7d03-4353-b3b2-7c4ca88a6330'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208734001, 60000, 'fd82bbaf-88c4-4b64-96b6-d5ea100015c9'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208794234, 60000, '822c4fd9-c786-4849-a7cd-9ce63fb2d6ee'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208854497, 60000, 'c053655a-28e5-4eea-bd43-483e442cccfe'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208914783, 60000, 'f66cab9c-fbb6-421b-b1a8-42cede6af7e2'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695208975029, 60000, 'ddaeef38-2bc9-43cd-a083-1cdb97e0a85a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209035290, 60000, 'ad946d4d-0887-4b7a-9eb1-65c93aef5ec5'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209095539, 60000, '6333fcd4-a51d-4fcd-b449-13dfc348f295'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209155789, 60000, '6fc32793-0831-4e2c-b7ec-629834ec4b89'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209216047, 60000, '1f115d15-06be-4b69-b52f-e26ac4d1c923'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209276309, 60000, '978e21e8-ca45-4571-99e2-9e29e728a32c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209336562, 60000, '607f985e-e927-400f-8f8e-701661772c91'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209396829, 60000, '6e4a2083-8f49-4d38-a3a5-9652d8fee811'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209457069, 60000, '20acb8fe-79a2-4d63-b607-790de67ea918'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209517322, 60000, 'acb710ca-0600-4a30-b784-94530c155ce2'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209577577, 60000, 'aa0b2cdb-1bc7-43b7-8517-54c8ef1b5518'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209637822, 60000, '33475ffd-ab20-4064-80e0-03e710dbb7d5'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209698064, 60000, '88163bae-e57c-4863-89a9-5ce8862d412b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209758311, 60000, '363f7604-c2ea-4211-8bc6-5a35bc66119a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209818566, 60000, 'bb6ee8b3-7968-46c2-bf4a-b33b0b8aa623'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209878815, 60000, '6cd549c0-878f-4fe1-a365-b1621dbbb692'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209939072, 60000, 'd0d12685-1cfc-45ee-bb68-098c4870caff'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695209999345, 60000, 'c65e94e8-a2ba-436d-aaf1-c010a2bbf1d0'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210059598, 60000, '0114c5f9-0fb3-4e1d-bc14-1b3710ea831d'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210119851, 60000, 'e56fad21-5ca6-49dc-acb5-6c2a1cbab640'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210180119, 60000, 'dec31d6d-d0b7-433a-ae65-6abbf2a927ce'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210240372, 60000, '3a102ace-b301-4108-93f5-0a4650f459e1'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210300643, 60000, '477260f9-9285-44e0-af58-0fdcfb39f4e0'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210360926, 60000, '65abc39f-1d03-4f3d-8db4-32652a6df216'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210421168, 60000, 'a636d551-159a-40d7-82df-70e7b0c0ec14'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210481435, 60000, 'b8f86ea2-e7b6-4268-8c66-0be2bc5f7d75'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210541691, 60000, '95ec3530-6616-47bb-a0ce-01707d0ebd49'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210601976, 60000, '421d69df-8bda-4863-8ad5-f47593d28027'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210662248, 60000, 'e07dcbcd-36e4-4b83-a7d7-fa303ec4e5ca'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210722613, 60000, '446ba085-46f4-4f81-bdd1-6b37ce49ef45'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210782876, 60000, 'a0918ea0-8026-44d6-b04d-a1f1c65ff049'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210843144, 60000, '36f29613-de1e-4574-b12a-94a68747e1e5'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210903467, 60000, '358b182f-eca0-46f6-803c-1e7e0efc3f28'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695210963730, 60000, '497e1529-5b21-47bd-862b-02bf9e319dfa'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211023962, 60000, '0b137ded-6621-43e3-ad28-9fdae4e088bc'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211084241, 60000, '7b89cf3d-ee7b-46c4-a22d-0fc387585498'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211144486, 60000, 'da8f9b53-66bc-43d5-b12f-a02b70fa1812'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211204816, 60000, 'a0a521fc-22bc-4aae-91bf-49804e7cdcde'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211265137, 60000, '8eb86cfa-a831-4bd1-9848-1e032392b708'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211325446, 60000, 'f595da12-a76a-4a0b-810d-b3a4be024b8c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211385712, 60000, '6c9a3514-316a-4468-8ede-fbedc1d06ce5'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211446021, 60000, '9d48c8ae-17f3-4512-9007-8db2524f327f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211506293, 60000, '913daf55-c450-4d7b-9bf9-20948b77856e'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211566554, 60000, '60373891-24a4-4d4e-bbf3-492996c1374c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211626827, 60000, '96413c12-d4fc-417b-8455-a9efb841920a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211687553, 60000, '6e7b6ac2-17ae-4a02-b974-6da1c35c2250'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211748069, 60000, '2d5de73e-2c5c-4553-967f-18c8adcec211'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211808351, 60000, 'e67e3c12-f73a-4aed-8eef-45262b6b2f0a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211868621, 60000, '1aad7ebc-5b0f-4020-a3f1-9bab43bd130f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211928886, 60000, 'f955859d-f8b1-4d00-8330-10f8347452d7'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695211989171, 60000, '4c42e375-02e4-40aa-b4cc-671f20006a59'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212049439, 60000, '8a52fd3b-8c5b-4386-addc-7f32fa496f94'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212109700, 60000, '924c4be8-abd2-46d9-ba2e-91524809e686'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212169966, 60000, 'bbf29d09-33ae-4f6b-be1d-a47e0e5e0737'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212230241, 60000, '5aa44045-3642-4e5c-9711-4841b5681d92'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212290503, 60000, '470c4b7e-bfc9-4406-b275-f630c7350d33'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212350768, 60000, '09946130-38bc-44ee-b3d1-90e4323e065c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212411055, 60000, 'f0418e24-716a-4398-a5aa-1601a1b23bfb'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212471315, 60000, 'eeb470cf-4d46-48e4-8c00-f7f4e86e83c3'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212531573, 60000, 'f4d34f46-c97d-4b52-8449-841e4b1238bf'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212591829, 60000, 'be23a391-731f-4a7b-abd5-836e4f681747'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212652111, 60000, '2793ebaa-62c6-40f6-b260-a39e9d4eac76'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212712387, 60000, 'e17b5904-eba7-4cad-b71a-b541068f745f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212772645, 60000, 'f51d2efa-dd23-4176-adeb-8d740b72bc41'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212832905, 60000, '8eb68ed4-3edd-4c0f-bbff-a9731e31413f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212893185, 60000, '61875057-cd7b-47a1-9dbf-e0ade78ba62b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695212953474, 60000, '474b6909-7cb6-4be0-835b-4d7e456b9ea1'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213013731, 60000, '9ad2c665-a6ac-45a1-aee9-a289dcd1fb73'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213073994, 60000, '241d0ca1-fd32-4da4-bd3b-f29b46f977e7'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213134260, 60000, '8f13d5d5-3538-461b-9292-cc34fc17c774'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213194524, 60000, '5fa04667-c51d-409c-9461-9b5491e3349a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213254806, 60000, '92724334-01e5-4d83-8f58-8879b58433c7'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213315078, 60000, 'ec6b15c0-7fbf-4356-9dde-826f043aa7db'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213375377, 60000, '0d15ed6c-2172-4494-b9d0-49531733cfec'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213435657, 60000, 'bcfd1597-2224-45a3-9639-97cc5e93cfb2'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213495926, 60000, '644f1bbc-92d2-4821-943e-47f7d99ec35d'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213556200, 60000, '58f25b8f-1846-4187-818d-a68b86a6d720'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213616482, 60000, '0fb4ad31-06f7-41c2-8c67-3408a6ac77fb'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213676758, 60000, '79cdb279-35fd-4964-803b-62149549cb9c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213737020, 60000, '1e6b0aef-1df2-49c3-a5f8-78c53ae020aa'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213797290, 60000, '3ed0412b-038e-458c-ab90-98fa40afbe4d'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213857560, 60000, '831cf5f4-4140-456c-a14e-83e622a736ee'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213917822, 60000, '430b44e1-1712-4925-8ae9-87fddbeb412e'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695213978116, 60000, '764c7a26-82f3-456a-8a1c-cb1edb0d2207'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214038445, 60000, '18bb2a60-ac4e-4f5d-87e4-58e41759a4e1'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214098714, 60000, 'cf3a2416-f917-4af2-a5d5-d2c395861f76'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214158968, 60000, '1fba22ed-e697-492b-8f53-8b1617804248'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214219214, 60000, '51f5d170-f5ba-48a6-925b-606596fb15ca'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214279464, 60000, 'c509e43c-9103-4de0-a01c-bc529494002c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214339729, 60000, '083f74a1-8e9d-44f3-91b0-75c8c53afacd'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214399970, 60000, '3dbabac2-432e-42be-9cce-d2762d4eef50'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214460219, 60000, '4608d517-b691-474e-94e6-ec0102f839b6'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214520473, 60000, '030422b6-6574-40da-a672-d41d6d7d701e'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214580726, 60000, '11d37a34-ef21-4698-8639-2a4dab72553a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214640977, 60000, '97b0700f-00d0-443e-a9e5-0a855333e601'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214701227, 60000, '08115fc1-8714-41da-87c9-439d0b2af0b1'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214761493, 60000, 'f063fe96-d0c6-49d5-ad75-71da3b475849'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214821758, 60000, '72c944e7-9953-4a02-be26-6f95bc3fa357'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214882003, 60000, 'c8b0f0c3-62f5-4ee1-9ef3-670703be5bd6'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695214942297, 60000, 'a7d51b4b-fbfe-41ea-ac7a-b710a6d75897'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215002928, 60000, '0419b198-7165-4253-ba97-a20ebf2f96a8'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215063601, 60000, '86f773cd-0e43-430b-9105-f3641d464899'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215124190, 60000, 'c0171539-be72-479b-bb73-77b68649230b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215184722, 60000, '1276a425-cff9-4e04-ba9f-26511cad528b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215245255, 60000, '3b78d677-0a74-4f15-9d5c-18e662eb61da'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215305864, 60000, '1ce94378-a09d-4723-8644-8076e8cf8b8a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215366459, 60000, '142c2de3-c62a-4bd3-96ea-8f9799d1eb63'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215427090, 60000, '76eff2f3-66ff-4e75-83c7-10943ab8cc06'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215487690, 60000, '9990a113-6ceb-40e6-8a2a-48ecd0a04400'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215548308, 60000, '8ea71d36-68fc-4367-b08a-a836ea0532df'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215608921, 60000, '2ddb3989-965b-48f7-b090-e714e6bc4c3d'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215669522, 60000, 'f32194d1-13e2-4581-bba8-a5dd86a42004'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215730125, 60000, 'babb1962-faf0-44d1-9b91-34e1dfbe3677'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215790760, 60000, '829f65ca-f175-4f4b-b1aa-cd028c13e49f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215851394, 60000, 'd5ceeb0e-eaed-4c9b-b807-109c9b6547b1'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215912023, 60000, '5c5ea417-e788-4099-a699-19cd37b76945'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695215972644, 60000, '14e8a4f3-ab84-48b4-88e0-9549440fa95f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216033259, 60000, '765ecafb-44cc-4a7c-a5ce-828b4919729b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216093883, 60000, 'f0e33d91-6c6c-425b-a4b6-cba41590f029'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216154504, 60000, '87a59629-0dd7-4aec-bf80-2213de732711'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216215108, 60000, '850e8ab1-c76d-4c68-93ae-3b213cc7d031'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216275727, 60000, '2bcd7495-4da5-42d0-9a92-d9d32f755000'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216336373, 60000, 'de49c1f0-1e66-4980-98e4-23fd4c1e00cb'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216397016, 60000, 'b62d6656-6696-4bc6-ae71-4f95c66b8360'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216457640, 60000, '0389736b-b491-424f-bed2-671d4a96383c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216518281, 60000, '4c486f7f-ac6f-46f4-9672-c8ce5b132272'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216578911, 60000, '0142fa88-648e-4302-baf4-ba8fb081e291'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216639534, 60000, 'fb182422-9dce-4b36-9863-9b6673982b3c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216700144, 60000, '352ce55e-1393-45a7-8201-b0dee4c2541f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216760746, 60000, '8e3ea30f-7359-4c4c-ae68-fc01dbc39697'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216821373, 60000, '04a317fa-8b93-4052-afb3-5c46d9a9e44d'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216882029, 60000, '9ead3d6a-204d-4bb9-b45d-a359c9f7a8af'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695216942664, 60000, '873aa530-8524-453c-af2f-282c6e67a7f9'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217003279, 60000, 'b92a3d76-24b7-4efb-b563-6709306176b3'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217063910, 60000, '2313bf50-5f64-4c45-8ba5-20041a30ddeb'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217124536, 60000, '4c4a3a55-0786-4abf-b7a9-97d3240b93e8'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217185157, 60000, '978a0783-a979-47d2-bf16-86795e32a1b9'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217245780, 60000, 'cf3a877b-79f9-4ab8-9030-8c965d750cba'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217306429, 60000, 'f30ebf08-fa35-4f26-95b4-1c9b1eecc3c9'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217367085, 60000, 'bd53c6e9-0eee-4fe6-b833-ddaafe6098c3'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217427699, 60000, 'f4fee769-85c5-4db1-a204-c8db6e0f916f'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217488306, 60000, '3a118c7e-a1ed-4cef-bc26-af7a837a3b25'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217548918, 60000, '87107235-4a45-4811-9b03-3f8f8b2788cb'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217609620, 60000, 'ce853721-a41c-4e28-a287-7a7f9541857b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217670247, 60000, '4c327909-ecb3-49ad-9b43-7e283e694dec'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217730850, 60000, 'adc0bb07-2664-4493-9767-f52ccfc3e4a6'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217791252, 60000, '3f417fe6-2854-45b9-935f-7de79b64c1bf'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217851511, 60000, 'd66dfda4-aaef-48dc-a1ec-73ceee38ab26'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217911769, 60000, '2e5431bc-40be-4c0a-98df-c43d85e828a2'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695217972022, 60000, '53d8bfa4-e204-412e-b715-9eeed541e1b7'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218032277, 60000, '8e0dc6af-e2b8-4f74-9426-3f52428edabf'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218092532, 60000, 'e99c185e-2f8d-4515-9b3f-fdf5b7211ea5'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218152828, 60000, '52993024-bf3d-4093-897b-c9995a8ffcb6'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218213088, 60000, 'e6e57f51-7bc9-46ab-a1f2-864c2fd73404'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218273333, 60000, 'cb785654-4760-4167-942c-eee463572240'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218333581, 60000, '628b6548-5287-4a33-8b65-53af36c8a153'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218393824, 60000, '93f8f29e-449a-4d51-afa0-10cecfb6c06e'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218454073, 60000, 'd13e0440-b278-40ba-b49f-414476e68340'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218514355, 60000, '289deb73-d591-412f-b478-e2d9d9b9484c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218574641, 60000, '46e7406a-272e-4673-974c-456e5b2cf8ca'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218634926, 60000, '8aafe444-64b0-4191-b44f-8732737bcdda'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218695204, 60000, '832db41b-5574-473b-8165-bbe92bff7d2a'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218755490, 60000, '61289f09-28c1-4078-b418-4490f93a12a4'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218815763, 60000, '957e10b5-e721-41bd-a16c-a9fa37adc965'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218876047, 60000, 'c403947a-b0ba-4e2b-a047-1cb20f5d8d92'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218936301, 60000, '07a6e41f-636b-4d03-b3e8-edf832dfa6f5'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695218996571, 60000, '2192254a-50f1-4bbb-ae78-62f8c4afe7be'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219056849, 60000, '020136b3-b44b-42e2-86db-c2f5659ea86e'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219117118, 60000, '2c1dd02e-3fa9-4079-abdc-db624332a0a9'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219177408, 60000, 'ccb3959e-fe5c-443c-8bde-d3dbe4b9fab3'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219237684, 60000, 'fdf217ac-a3a9-4103-a2c9-396c44bea4ca'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219297943, 60000, 'e3d73b2c-b259-4704-b75f-4f634280f224'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219358204, 60000, 'de9b2b16-fb62-442b-a749-318660449f5c'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219418458, 60000, '3c32b9fc-41c3-4542-8b64-81ee15d98738'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219478721, 60000, 'cefe9a7e-bf4d-4c35-9c76-e0cbdc336689'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219538990, 60000, '82457869-79d2-4772-b364-1141a6ef984b'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219599252, 60000, 'ba1e9e62-e3db-41c7-9af6-590dd27e7182'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219659526, 60000, '037352ac-030e-4a69-91a7-3dfcf0f7e433'); -INSERT INTO public.edc_lease VALUES ('example-connector', 1695219719803, 60000, '34ab855d-4f66-4b9f-95f7-a79cd9f10bf0'); - - --- --- Data for Name: edc_policydefinitions; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_policydefinitions VALUES ('always-true', '[{"edctype":"dataspaceconnector:permission","uid":null,"target":null,"action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"ALWAYS_TRUE"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"true"},"operator":"EQ"}],"duties":[]}]', '[]', '[]', '{}', NULL, NULL, NULL, NULL, '{"@policytype":"set"}', 1695137823306); -INSERT INTO public.edc_policydefinitions VALUES ('first-policy', '[{"edctype":"dataspaceconnector:permission","uid":null,"target":null,"action":{"type":"USE","includedIn":null,"constraint":null},"assignee":null,"assigner":null,"constraints":[{"edctype":"AtomicConstraint","leftExpression":{"edctype":"dataspaceconnector:literalexpression","value":"POLICY_EVALUATION_TIME"},"rightExpression":{"edctype":"dataspaceconnector:literalexpression","value":"2023-08-31T22:00:00.000Z"},"operator":"GEQ"}],"duties":[]}]', '[]', '[]', '{}', NULL, NULL, NULL, NULL, '{"@policytype":"set"}', 1695207922457); - - --- --- Data for Name: edc_transfer_process; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_transfer_process VALUES ('27075fc4-b18f-44e1-8bde-a9f62817dab2', 'PROVIDER', 600, 1, 1695208010855, 1695208010083, '{}', NULL, '{"definitions":[]}', NULL, '{"properties":{"baseUrl":"http://localhost:23001/api/test-backend/data-source","method":"GET","queryParams":"data=first-asset-data","type":"HttpData"}}', '[]', '34ab855d-4f66-4b9f-95f7-a79cd9f10bf0', 1695208010855, '{}'); - - --- --- Data for Name: edc_data_request; Type: TABLE DATA; Schema: public; Owner: edc --- - -INSERT INTO public.edc_data_request VALUES ('f9a60bc8-0cb5-4f30-8604-2e3b1d020541', '27075fc4-b18f-44e1-8bde-a9f62817dab2', 'http://localhost:23003/api/v1/ids/data', 'ids-multipart', 'urn:connector:example-connector', 'urn:artifact:first-asset:1.0', 'first-cd:28356d13-7fac-4540-80f2-22972c975ecb', '{"properties":{"baseUrl":"https://webhook.site/6d5008a7-8c29-4e14-83c1-cc64f86d5398","method":"POST","type":"HttpData"}}', true, '{}', '{"contentType":"application/octet-stream","isFinite":true}', '27075fc4-b18f-44e1-8bde-a9f62817dab2'); diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index b6b1eab88..667268a3f 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { api(project(":utils:json-and-jsonld-utils")) implementation(project(":extensions:policy-always-true")) + implementation(project(":extensions:postgres-flyway")) implementation(project(":utils:versions")) implementation(libs.edc.jsonLdSpi) implementation(libs.edc.jsonLd) diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java index 147debb37..320b6a05e 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/connector/DataTransferTestUtil.java @@ -37,20 +37,20 @@ public class DataTransferTestUtil { public static JsonObject buildDataAddressJsonLd(String baseUrl, String method) { return createObjectBuilder() - .add(TYPE, EDC_NAMESPACE + "DataAddress") - .add(EDC_NAMESPACE + "type", "HttpData") - .add(EDC_NAMESPACE + "properties", createObjectBuilder() - .add(EDC_NAMESPACE + "baseUrl", baseUrl) - .add(EDC_NAMESPACE + "method", method) - .build()) - .build(); + .add(TYPE, EDC_NAMESPACE + "DataAddress") + .add(EDC_NAMESPACE + "type", "HttpData") + .add(EDC_NAMESPACE + "properties", createObjectBuilder() + .add(EDC_NAMESPACE + "baseUrl", baseUrl) + .add(EDC_NAMESPACE + "method", method) + .build()) + .build(); } public static Map buildDataAddressProperties(String baseUrl, String method) { return Map.of( - EDC_NAMESPACE + "type", "HttpData", - EDC_NAMESPACE + "baseUrl", baseUrl, - EDC_NAMESPACE + "method", method + EDC_NAMESPACE + "type", "HttpData", + EDC_NAMESPACE + "baseUrl", baseUrl, + EDC_NAMESPACE + "method", method ); } @@ -58,11 +58,11 @@ public static Map buildDataAddressProperties(String baseUrl, Str public static void validateDataTransferred(String checkUrl, String expectedData) { await().atMost(TIMEOUT).untilAsserted(() -> { var actual = - when() - .get(checkUrl) - .then() - .statusCode(200) - .extract().body().asString(); + when() + .get(checkUrl) + .then() + .statusCode(200) + .extract().body().asString(); assertThat(actual).isEqualTo(expectedData); }); } @@ -70,11 +70,11 @@ public static void validateDataTransferred(String checkUrl, String expectedData) public static void validateDataTransferred(String checkUrl, Map params, String expected) { await().atMost(TIMEOUT).untilAsserted(() -> { var actual = - given().params(params).when() - .get(checkUrl) - .then() - .statusCode(200) - .extract().body().asString(); + given().params(params).when() + .get(checkUrl) + .then() + .statusCode(200) + .extract().body().asString(); assertThat(actual).isEqualTo(expected); }); } diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java index a8560544e..f54dc26b3 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java @@ -55,7 +55,6 @@ import java.util.stream.Stream; import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.RUNNING; -import static de.sovity.edc.extension.policy.AlwaysTruePolicyConstants.POLICY_DEFINITION_ID; import static java.time.Duration.ofSeconds; import static org.assertj.core.api.Assertions.assertThat; @@ -84,8 +83,6 @@ public E2eScenario(ConnectorConfig consumerConfig, ConnectorConfig providerConfi .build(); } - private final String alwaysTruePolicyId = POLICY_DEFINITION_ID; - private final AtomicInteger assetCounter = new AtomicInteger(0); public String createAsset() { @@ -150,7 +147,7 @@ private IdResponseDto internalCreateAsset(String assetId, UiDataSource dataSourc } public String createContractDefinition(String assetId) { - return createContractDefinition(alwaysTruePolicyId, assetId).getId(); + return createContractDefinition("always-true", assetId).getId(); } public IdResponseDto createContractDefinition(String policyId, String assetId) { diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java index 95f45ec4b..e20f6e646 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java @@ -20,6 +20,7 @@ import de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfig; import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.utils.Lazy; +import lombok.Builder; import lombok.val; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.extension.AfterAllCallback; @@ -36,6 +37,7 @@ import java.util.stream.Stream; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static de.sovity.edc.extension.postgresql.PostgresFlywayExtension.EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS; import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; import static org.mockserver.stop.Stop.stopQuietly; @@ -43,10 +45,12 @@ public class E2eTestExtension implements BeforeAllCallback, AfterAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { private final String consumerParticipantId; + private final String additionalConsumerMigrationLocation; private ConnectorConfig consumerConfig; private final EdcRuntimeExtensionWithTestDatabase consumerExtension; - private final String providerParticipantId; + private String providerParticipantId; + private final String additionalProviderMigrationLocation; private ConnectorConfig providerConfig; private final EdcRuntimeExtensionWithTestDatabase providerExtension; @@ -57,18 +61,31 @@ public class E2eTestExtension private Lazy clientAndServer; public E2eTestExtension() { - this("consumer", "provider"); + this("consumer", "provider", "", ""); } - public E2eTestExtension(String consumerParticipantId, String providerParticipantId) { + @Builder + public E2eTestExtension(String additionalConsumerMigrationLocation, String additionalProviderMigrationLocation) { + this("consumer", "provider", additionalConsumerMigrationLocation, additionalProviderMigrationLocation); + } + + public E2eTestExtension( + String consumerParticipantId, + String providerParticipantId, + String additionalConsumerMigrationLocation, + String additionalProviderMigrationLocation + ) { this.consumerParticipantId = consumerParticipantId; this.providerParticipantId = providerParticipantId; + this.additionalConsumerMigrationLocation = additionalConsumerMigrationLocation; + this.additionalProviderMigrationLocation = additionalProviderMigrationLocation; consumerExtension = new EdcRuntimeExtensionWithTestDatabase( ":launchers:connectors:sovity-dev", "consumer", testDatabase -> { consumerConfig = forTestDatabase(this.consumerParticipantId, testDatabase); + consumerConfig.getProperties().put(EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS, this.additionalConsumerMigrationLocation); return consumerConfig.getProperties(); } ); @@ -77,6 +94,7 @@ public E2eTestExtension(String consumerParticipantId, String providerParticipant "provider", testDatabase -> { providerConfig = forTestDatabase(this.providerParticipantId, testDatabase); + providerConfig.getProperties().put(EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS, this.additionalProviderMigrationLocation); return providerConfig.getProperties(); } ); From 1eb8fcbb2989c874cfb94aee3a914f8e5c3ad33b Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 24 Jul 2024 15:30:22 +0300 Subject: [PATCH 270/295] chore: prepare release (#1007) --- CHANGELOG.md | 53 ++++++++++++++++--- .../postgresql/FlywayExecutionParams.java | 1 - .../api/common/mappers/PolicyMapperTest.java | 7 --- .../ext/wrapper/api/ui/UiResourceImpl.java | 4 +- .../edc/e2e/ContractTerminationTest.java | 1 - .../AlwaysTruePolicyMigrationCommonTest.java | 1 - 6 files changed, 47 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd863ca60..15ba7f037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,32 +9,69 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Detailed Changes +#### Minor + +#### Patch + +#### Major Changes + +#### Minor Changes + +### Deployment Migration Notes + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:{{ VERSION }}` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:{{ VERSION }}` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:{{ VERSION }}` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:{{ VERSION }}` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` + +## [10.0.0] - 2024-07-24 + +### Overview + +MDS 2.2 release + +### Detailed Changes + #### Major Changes -- The `UiPolicy` model has been adjusted to support complex expressions including `AND`, `OR` and `XONE`. +- Complex policies using AND, OR and XONE: + - Complex policy support in the Connector UI. + - The `UiPolicy` model has been adjusted to support complex expressions including `AND`, `OR` and `XONE`. - The `createPolicyDefinition` has been marked as deprecated in favor of the new `createPolicyDefinitionV2` endpoint that supports complex policies. - Removed the recently rushed `createPolicyDefinitionUseCase` endpoint in favor of the new `createPolicyDefinitionV2` endpoint. #### Minor Changes +- Reworked data offer creation page for easier data sharing. - Both providers and consumers can now terminate their contracts. -- Added endpoints for checking ID availability for policies, assets and contract definitions +- Contracts can be filtered by their termination status. +- Improved "On Request" data offer support in the Connector UI. - The always-true policy is now created with no constraints instead of the artificial `ALWAYS_TRUE = TRUE` constraint - Existing always-true policy definitions are migrated to the new format - existing contract agreements are not affected #### Patch Changes +- Fixed an issue that caused the auth information to get lost during asset + creation. + ### Deployment Migration Notes +_No special migration notes required_ + #### Compatible Versions - Connector Backend Docker Images: - - Dev EDC: `ghcr.io/sovity/edc-dev:{{ VERSION }}` - - sovity EDC CE: `ghcr.io/sovity/edc-ce:{{ VERSION }}` - - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:{{ VERSION }}` - - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:{{ VERSION }}` - - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` -- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` + - Dev EDC: `ghcr.io/sovity/edc-dev:10.0.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.0.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.0.0` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.0.0` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.0.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.0` ## [9.0.0] - 2024-07-15 diff --git a/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java index f56da9d6b..540b0524c 100644 --- a/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java +++ b/extensions/postgres-flyway-core/src/main/java/de/sovity/edc/extension/postgresql/FlywayExecutionParams.java @@ -15,7 +15,6 @@ package de.sovity.edc.extension.postgresql; import lombok.Builder; -import lombok.Singular; import lombok.Value; import lombok.With; diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java index 7ef734cfe..adfcbbe2a 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/PolicyMapperTest.java @@ -17,18 +17,12 @@ import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionExtractor; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.ExpressionMapper; import de.sovity.edc.ext.wrapper.api.common.mappers.policy.MappingErrors; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyConstraint; -import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; import jakarta.json.Json; import jakarta.json.JsonObject; -import org.eclipse.edc.policy.model.AndConstraint; -import org.eclipse.edc.policy.model.AtomicConstraint; import org.eclipse.edc.policy.model.Constraint; -import org.eclipse.edc.policy.model.OrConstraint; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.policy.model.PolicyType; -import org.eclipse.edc.policy.model.XoneConstraint; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.Test; @@ -37,7 +31,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index e43a992fd..ab856602f 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -14,8 +14,6 @@ package de.sovity.edc.ext.wrapper.api.ui; -import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; -import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; @@ -31,6 +29,8 @@ import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateTransferRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateRequest; import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionPage; import de.sovity.edc.ext.wrapper.api.ui.model.TransferHistoryPage; import de.sovity.edc.ext.wrapper.api.ui.model.UiContractNegotiation; diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index 4b71abef4..41944d5bf 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -54,7 +54,6 @@ import static java.time.Duration.ofSeconds; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.DynamicTest.dynamicTest; diff --git a/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java b/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java index 46d487d6c..4180dcd0c 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java @@ -14,7 +14,6 @@ import java.util.Map; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; public class AlwaysTruePolicyMigrationCommonTest { From f7895eca8fccb76b9afffef40a0fe112c49b9d13 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 24 Jul 2024 16:07:56 +0300 Subject: [PATCH 271/295] chore: fix always-true policy test (#1008) --- .../AlwaysTrueMigrationNewProviderTest.java | 58 ------------------- ...Test.java => AlwaysTrueMigrationTest.java} | 48 +++++++++++++-- .../AlwaysTruePolicyMigrationCommonTest.java | 50 ---------------- 3 files changed, 44 insertions(+), 112 deletions(-) delete mode 100644 tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java rename tests/src/test/java/de/sovity/edc/e2e/{AlwaysTrueMigrationNewConsumerTest.java => AlwaysTrueMigrationTest.java} (55%) delete mode 100644 tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java deleted file mode 100644 index bd2ea7b6b..000000000 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewProviderTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.sovity.edc.e2e; - -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.client.gen.model.OperatorDto; -import de.sovity.edc.client.gen.model.UiPolicyExpressionType; -import de.sovity.edc.e2e.common.AlwaysTruePolicyMigrationCommonTest; -import de.sovity.edc.extension.e2e.extension.Consumer; -import de.sovity.edc.extension.e2e.extension.E2eScenario; -import de.sovity.edc.extension.e2e.extension.E2eTestExtension; -import de.sovity.edc.extension.e2e.extension.Provider; -import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; -import de.sovity.edc.extension.utils.junit.DisabledOnGithub; -import lombok.val; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockserver.integration.ClientAndServer; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -class AlwaysTrueMigrationNewProviderTest { - - @RegisterExtension - private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() - .additionalConsumerMigrationLocation("classpath:db/additional-test-data/alwaysTruePolicyMigrationTest") - .additionalProviderMigrationLocation("") - .build(); - - @Test - @DisabledOnGithub - void always_true_agreements_still_work_after_migration( - E2eScenario scenario, - ClientAndServer mockServer, - @Provider EdcClient providerClient, - @Consumer EdcClient consumerClient - ) { - // assert correct policies - val newAlwaysTruePolicyOnProvider = providerClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter( - policy -> policy.getPolicyDefinitionId().equals(AlwaysTruePolicyConstants.POLICY_DEFINITION_ID) - ).findFirst().orElseThrow().getPolicy().getExpression(); - - val oldAlwaysTruePolicyOnConsumer = consumerClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter( - policy -> policy.getPolicyDefinitionId().equals(AlwaysTruePolicyConstants.POLICY_DEFINITION_ID) - ).findFirst().orElseThrow().getPolicy().getExpression(); - - assertThat(oldAlwaysTruePolicyOnConsumer.getType()).isEqualTo(UiPolicyExpressionType.CONSTRAINT); - assertThat(oldAlwaysTruePolicyOnConsumer.getConstraint().getLeft()).isEqualTo(AlwaysTruePolicyConstants.EXPRESSION_LEFT_VALUE); - assertThat(oldAlwaysTruePolicyOnConsumer.getConstraint().getRight().getValue()).isEqualTo( - AlwaysTruePolicyConstants.EXPRESSION_RIGHT_VALUE); - assertThat(oldAlwaysTruePolicyOnConsumer.getConstraint().getOperator()).isEqualTo(OperatorDto.EQ); - - assertThat(newAlwaysTruePolicyOnProvider.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); - assertThat(newAlwaysTruePolicyOnProvider.getConstraint()).isNull(); - - AlwaysTruePolicyMigrationCommonTest.alwaysTruePolicyMigrationTest(scenario, mockServer, providerClient, consumerClient); - } - - -} diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java similarity index 55% rename from tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java rename to tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java index 69e206892..5958d5d7e 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationNewConsumerTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java @@ -1,23 +1,31 @@ package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; +import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; import de.sovity.edc.client.gen.model.UiPolicyExpressionType; -import de.sovity.edc.e2e.common.AlwaysTruePolicyMigrationCommonTest; import de.sovity.edc.extension.e2e.extension.Consumer; import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.ws.rs.HttpMethod; import lombok.val; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -class AlwaysTrueMigrationNewConsumerTest { +class AlwaysTrueMigrationTest { @RegisterExtension private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() @@ -27,7 +35,7 @@ class AlwaysTrueMigrationNewConsumerTest { @Test @DisabledOnGithub - void always_true_agreements_still_work_after_migration( + void test_migrated_policy_working_test_legacy_policy_working( E2eScenario scenario, ClientAndServer mockServer, @Provider EdcClient providerClient, @@ -52,6 +60,38 @@ void always_true_agreements_still_work_after_migration( assertThat(migratedAlwaysTruePolicy.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); assertThat(migratedAlwaysTruePolicy.getConstraint()).isNull(); - AlwaysTruePolicyMigrationCommonTest.alwaysTruePolicyMigrationTest(scenario, mockServer, providerClient, consumerClient); + testTransfer(scenario, mockServer, providerClient, consumerClient); + testTransfer(scenario, mockServer, consumerClient, providerClient); + } + + public void testTransfer(E2eScenario scenario, ClientAndServer mockServer, EdcClient providerClient, EdcClient consumerClient) { + // arrange + val destinationPath = "/destination/some/path/"; + val destinationUrl = "http://localhost:" + mockServer.getPort() + destinationPath; + mockServer.when(HttpRequest.request(destinationPath).withMethod("POST")).respond(it -> HttpResponse.response().withStatusCode(200)); + + val asset = scenario.createAsset(); + scenario.createContractDefinition(asset); //this automatically uses the always-true policy + val negotiation = scenario.negotiateAssetAndAwait(asset); + + // act + val transfer = scenario.transferAndAwait( + InitiateTransferRequest.builder() + .contractAgreementId(negotiation.getContractAgreementId()) + .dataSinkProperties( + Map.of( + Prop.Edc.BASE_URL, destinationUrl, + Prop.Edc.METHOD, HttpMethod.POST, + Prop.Edc.TYPE, "HttpData" + ) + ) + .build() + ); + val transferProcess = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().stream().filter( + process -> process.getTransferProcessId().equals(transfer) + ).findFirst().orElseThrow(); + + // assert + AssertionsForClassTypes.assertThat(transferProcess.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); } } diff --git a/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java b/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java deleted file mode 100644 index 4180dcd0c..000000000 --- a/tests/src/test/java/de/sovity/edc/e2e/common/AlwaysTruePolicyMigrationCommonTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.sovity.edc.e2e.common; - -import de.sovity.edc.client.EdcClient; -import de.sovity.edc.client.gen.model.InitiateTransferRequest; -import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; -import de.sovity.edc.extension.e2e.extension.E2eScenario; -import de.sovity.edc.utils.jsonld.vocab.Prop; -import jakarta.ws.rs.HttpMethod; -import lombok.val; -import org.mockserver.integration.ClientAndServer; -import org.mockserver.model.HttpRequest; -import org.mockserver.model.HttpResponse; - -import java.util.Map; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -public class AlwaysTruePolicyMigrationCommonTest { - - public static void alwaysTruePolicyMigrationTest(E2eScenario scenario, ClientAndServer mockServer, EdcClient providerClient, EdcClient consumerClient) { - // arrange - val destinationPath = "/destination/some/path/"; - val destinationUrl = "http://localhost:" + mockServer.getPort() + destinationPath; - mockServer.when(HttpRequest.request(destinationPath).withMethod("POST")).respond(it -> HttpResponse.response().withStatusCode(200)); - - val asset = scenario.createAsset(); - scenario.createContractDefinition(asset); //this automatically uses the always-true policy - val negotiation = scenario.negotiateAssetAndAwait(asset); - - // act - val transfer = scenario.transferAndAwait( - InitiateTransferRequest.builder() - .contractAgreementId(negotiation.getContractAgreementId()) - .dataSinkProperties( - Map.of( - Prop.Edc.BASE_URL, destinationUrl, - Prop.Edc.METHOD, HttpMethod.POST, - Prop.Edc.TYPE, "HttpData" - ) - ) - .build() - ); - val transferProcess = consumerClient.uiApi().getTransferHistoryPage().getTransferEntries().stream().filter( - process -> process.getTransferProcessId().equals(transfer) - ).findFirst().orElseThrow(); - - // assert - assertThat(transferProcess.getState().getSimplifiedState()).isEqualTo(TransferProcessSimplifiedState.OK); - } -} From 36d870b7dd5f8e1c8be01db2cf901b321d379daf Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 24 Jul 2024 17:01:14 +0300 Subject: [PATCH 272/295] chore: fix test (#1009) --- .../edc/e2e/AlwaysTrueMigrationTest.java | 5 ++- .../e2e/AlwaysTrueMigrationTestReversed.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java index 5958d5d7e..b80a7cfe1 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java @@ -60,11 +60,10 @@ void test_migrated_policy_working_test_legacy_policy_working( assertThat(migratedAlwaysTruePolicy.getType()).isEqualTo(UiPolicyExpressionType.EMPTY); assertThat(migratedAlwaysTruePolicy.getConstraint()).isNull(); - testTransfer(scenario, mockServer, providerClient, consumerClient); - testTransfer(scenario, mockServer, consumerClient, providerClient); + testTransfer(scenario, mockServer, consumerClient); } - public void testTransfer(E2eScenario scenario, ClientAndServer mockServer, EdcClient providerClient, EdcClient consumerClient) { + public static void testTransfer(E2eScenario scenario, ClientAndServer mockServer, EdcClient consumerClient) { // arrange val destinationPath = "/destination/some/path/"; val destinationUrl = "http://localhost:" + mockServer.getPort() + destinationPath; diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java new file mode 100644 index 000000000..8046a9f48 --- /dev/null +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java @@ -0,0 +1,35 @@ +package de.sovity.edc.e2e; + +import de.sovity.edc.client.EdcClient; +import de.sovity.edc.extension.e2e.extension.Consumer; +import de.sovity.edc.extension.e2e.extension.E2eScenario; +import de.sovity.edc.extension.e2e.extension.E2eTestExtension; +import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockserver.integration.ClientAndServer; + +import static de.sovity.edc.e2e.AlwaysTrueMigrationTest.testTransfer; +import static org.assertj.core.api.Assertions.assertThat; + +class AlwaysTrueMigrationTestReversed { + + @RegisterExtension + private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() + .additionalConsumerMigrationLocation("classpath:db/additional-test-data/always-true-policy-legacy") + .additionalProviderMigrationLocation("classpath:db/additional-test-data/always-true-policy-migrated") + .build(); + + @Test + @DisabledOnGithub + void test_migrated_policy_working_test_legacy_policy_working( + E2eScenario scenario, + ClientAndServer mockServer, + @Provider EdcClient providerClient, + @Consumer EdcClient consumerClient + ) { + // assert correct policies + testTransfer(scenario, mockServer, consumerClient); + } +} From 75fd7cdc90436ee533ef46b76a49c067925e6670 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 24 Jul 2024 18:42:49 +0300 Subject: [PATCH 273/295] fix: test (#1010) --- extensions/contract-termination/build.gradle.kts | 1 + .../contacttermination/query/TerminateContractQueryTest.java | 4 +++- extensions/wrapper/clients/java-client/README.md | 2 +- .../de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java | 1 - .../test/java/de/sovity/edc/e2e/ContractTerminationTest.java | 4 +++- utils/test-utils/build.gradle.kts | 4 ---- .../sovity/edc/extension/e2e/extension/E2eTestExtension.java | 5 ++--- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/extensions/contract-termination/build.gradle.kts b/extensions/contract-termination/build.gradle.kts index a220fc241..bcb87a44e 100644 --- a/extensions/contract-termination/build.gradle.kts +++ b/extensions/contract-termination/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { testImplementation(project(":utils:test-utils")) testImplementation(project(":utils:versions")) + testImplementation(libs.edc.monitorJdkLogger) testImplementation(libs.edc.http) { exclude(group = "org.eclipse.jetty", module = "jetty-client") exclude(group = "org.eclipse.jetty", module = "jetty-http") diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java index 1308eaf1c..5fb3c67ee 100644 --- a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java @@ -28,9 +28,11 @@ import org.junit.jupiter.api.extension.ExtendWith; import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.COUNTERPARTY; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; @ExtendWith(E2eTestExtension.class) class TerminateContractQueryTest { @@ -77,7 +79,7 @@ void terminateConsumerAgreementOrThrow_shouldInsertRowInTerminationTable( assertThat(detailsAfterTermination.consumerAgentId()).isEqualTo("consumer"); assertThat(detailsAfterTermination.reason()).isEqualTo("Some reason"); assertThat(detailsAfterTermination.detail()).isEqualTo("Some detail"); - assertThat(detailsAfterTermination.terminatedAt()).isBetween(now, now.plusSeconds(1)); + assertThat(detailsAfterTermination.terminatedAt()).isCloseTo(now, within(2, ChronoUnit.SECONDS)); assertThat(detailsAfterTermination.terminatedBy()).isEqualTo(COUNTERPARTY); } ); diff --git a/extensions/wrapper/clients/java-client/README.md b/extensions/wrapper/clients/java-client/README.md index 1c419272a..837fca3bd 100644 --- a/extensions/wrapper/clients/java-client/README.md +++ b/extensions/wrapper/clients/java-client/README.md @@ -72,7 +72,7 @@ public class WrapperClientExample { ```java import de.sovity.edc.client.EdcClient; import de.sovity.edc.client.gen.model.KpiResult; -import de.sovity.edc.client.oauth2.OAuth2ClientCredentials; +import de.sovity.edc.client.oauth2.Oauth2ClientCredentials; import de.sovity.edc.client.oauth2.SovityKeycloakUrl; /** diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java index 8046a9f48..9d9bf62a9 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java @@ -11,7 +11,6 @@ import org.mockserver.integration.ClientAndServer; import static de.sovity.edc.e2e.AlwaysTrueMigrationTest.testTransfer; -import static org.assertj.core.api.Assertions.assertThat; class AlwaysTrueMigrationTestReversed { diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index 41944d5bf..e55cc6f10 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -43,6 +43,7 @@ import org.mockserver.model.HttpResponse; import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.stream.IntStream; @@ -54,6 +55,7 @@ import static java.time.Duration.ofSeconds; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.DynamicTest.dynamicTest; @@ -449,7 +451,7 @@ private static void assertTermination( assertThat(consumerInformation).isNotNull(); val now = OffsetDateTime.now(); - assertThat(consumerInformation.getTerminatedAt()).isBetween(now.minusMinutes(1), now.plusMinutes(1)); + assertThat(consumerInformation.getTerminatedAt()).isCloseTo(now, within(1, ChronoUnit.MINUTES)); assertThat(consumerInformation.getDetail()).isEqualTo(detail); assertThat(consumerInformation.getReason()).isEqualTo(reason); diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index 667268a3f..bfff302da 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -16,12 +16,8 @@ dependencies { api(project(":extensions:wrapper:clients:java-client")) api(project(":utils:json-and-jsonld-utils")) - implementation(project(":extensions:policy-always-true")) - implementation(project(":extensions:postgres-flyway")) implementation(project(":utils:versions")) - implementation(libs.edc.jsonLdSpi) implementation(libs.edc.jsonLd) - implementation(libs.edc.sqlCore) implementation(libs.assertj.core) implementation(libs.jooq.jooq) implementation(libs.mockserver.netty) diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java index e20f6e646..0605a69cd 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java @@ -37,7 +37,6 @@ import java.util.stream.Stream; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; -import static de.sovity.edc.extension.postgresql.PostgresFlywayExtension.EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS; import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; import static org.mockserver.stop.Stop.stopQuietly; @@ -85,7 +84,7 @@ public E2eTestExtension( "consumer", testDatabase -> { consumerConfig = forTestDatabase(this.consumerParticipantId, testDatabase); - consumerConfig.getProperties().put(EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS, this.additionalConsumerMigrationLocation); + consumerConfig.getProperties().put("edc.flyway.additional.migration.locations", this.additionalConsumerMigrationLocation); return consumerConfig.getProperties(); } ); @@ -94,7 +93,7 @@ public E2eTestExtension( "provider", testDatabase -> { providerConfig = forTestDatabase(this.providerParticipantId, testDatabase); - providerConfig.getProperties().put(EDC_FLYWAY_ADDITIONAL_MIGRATION_LOCATIONS, this.additionalProviderMigrationLocation); + providerConfig.getProperties().put("edc.flyway.additional.migration.locations", this.additionalProviderMigrationLocation); return providerConfig.getProperties(); } ); From f8d349ea4057abfe6175dbab4c0aaf6158d75a37 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Mon, 29 Jul 2024 10:44:54 +0200 Subject: [PATCH 274/295] chore: Rework E2E test extension to be used in the EDC EE (#1011) --- docs/api/sovity-edc-api-wrapper.yaml | 4 +- .../ext/catalog/crawler/CrawlerE2eTest.java | 2 +- ...tAgreementTerminationDetailsQueryTest.java | 7 +-- .../query/TerminateContractQueryTest.java | 7 ++- ...a => AlwaysTrueMigrationReversedTest.java} | 15 ++++-- .../edc/e2e/AlwaysTrueMigrationTest.java | 27 ++++++++-- .../edc/e2e/ContractTerminationTest.java | 7 ++- .../e2e/DataSourceParameterizationTest.java | 8 ++- .../edc/e2e/DataSourceQueryParamsTest.java | 7 ++- .../edc/e2e/ManagementApiTransferTest.java | 7 ++- .../PlaceholderDataSourceExtensionTest.java | 7 ++- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 3 +- .../sovity/edc/e2e/UseCaseApiWrapperTest.java | 7 ++- .../e2e/db/EdcRuntimeExtensionFixed.java | 1 + .../CeE2eTestExtensionConfigFactory.java | 29 +++++++++++ .../e2e/extension/E2eTestExtension.java | 51 ++++++++----------- .../e2e/extension/E2eTestExtensionConfig.java | 46 +++++++++++++++++ .../edc/extension/e2e/extension/Helpers.java | 26 ++++++++++ 18 files changed, 201 insertions(+), 60 deletions(-) rename tests/src/test/java/de/sovity/edc/e2e/{AlwaysTrueMigrationTestReversed.java => AlwaysTrueMigrationReversedTest.java} (58%) create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/CeE2eTestExtensionConfigFactory.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtensionConfig.java create mode 100644 utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Helpers.java diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 3f9ca3a7f..4440e71de 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -584,11 +584,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource - default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM + default: CUSTOM SecretValue: type: object properties: @@ -796,7 +796,6 @@ components: UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource - default: GET enum: - GET - POST @@ -804,6 +803,7 @@ components: - PATCH - DELETE - OPTIONS + default: GET UiDataSourceOnRequest: required: - contactEmail diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java index 1882f283a..8ae17ae24 100644 --- a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/CrawlerE2eTest.java @@ -62,7 +62,7 @@ class CrawlerE2eTest { private static EdcClient connectorClient; @RegisterExtension - static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( + private static EdcRuntimeExtensionWithTestDatabase providerExtension = new EdcRuntimeExtensionWithTestDatabase( ":launchers:connectors:sovity-dev", "provider", testDatabase -> { diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java index a3baebb4b..ebc74740c 100644 --- a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/ContractAgreementTerminationDetailsQueryTest.java @@ -26,15 +26,16 @@ import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import lombok.val; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; - -@ExtendWith(E2eTestExtension.class) class ContractAgreementTerminationDetailsQueryTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = new E2eTestExtension(":launchers:connectors:sovity-dev"); + @DisabledOnGithub @Test void fetchAgreementDetailsOrThrow_whenAgreementIsPresent_shouldReturnTheAgreementDetails( diff --git a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java index 5fb3c67ee..b7000d35a 100644 --- a/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java +++ b/extensions/contract-termination/src/test/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQueryTest.java @@ -25,18 +25,21 @@ import lombok.val; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.COUNTERPARTY; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; -@ExtendWith(E2eTestExtension.class) class TerminateContractQueryTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + @DisabledOnGithub @Test void terminateConsumerAgreementOrThrow_shouldInsertRowInTerminationTable( diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java similarity index 58% rename from tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java rename to tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java index 9d9bf62a9..525e5669d 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTestReversed.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java @@ -11,14 +11,19 @@ import org.mockserver.integration.ClientAndServer; import static de.sovity.edc.e2e.AlwaysTrueMigrationTest.testTransfer; +import static de.sovity.edc.extension.e2e.extension.CeE2eTestExtensionConfigFactory.defaultBuilder; -class AlwaysTrueMigrationTestReversed { +class AlwaysTrueMigrationReversedTest { @RegisterExtension - private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() - .additionalConsumerMigrationLocation("classpath:db/additional-test-data/always-true-policy-legacy") - .additionalProviderMigrationLocation("classpath:db/additional-test-data/always-true-policy-migrated") - .build(); + private static final E2eTestExtension E2E_TEST_EXTENSION = new E2eTestExtension( + defaultBuilder().toBuilder() + .consumerConfigCustomizer(config -> config.getProperties() + .put("edc.flyway.additional.migration.locations", "classpath:db/additional-test-data/always-true-policy-legacy")) + .providerConfigCustomizer(config -> config.getProperties() + .put("edc.flyway.additional.migration.locations", "classpath:db/additional-test-data/always-true-policy-migrated")) + .build() + ); @Test @DisabledOnGithub diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java index b80a7cfe1..e1d08fabd 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationTest.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; @@ -23,15 +37,20 @@ import java.util.Map; +import static de.sovity.edc.extension.e2e.extension.CeE2eTestExtensionConfigFactory.withModule; import static org.assertj.core.api.Assertions.assertThat; class AlwaysTrueMigrationTest { @RegisterExtension - private static final E2eTestExtension E2E_TEST_EXTENSION = E2eTestExtension.builder() - .additionalConsumerMigrationLocation("classpath:db/additional-test-data/always-true-policy-migrated") - .additionalProviderMigrationLocation("classpath:db/additional-test-data/always-true-policy-legacy") - .build(); + private static final E2eTestExtension E2E_TEST_EXTENSION = new E2eTestExtension( + withModule(":launchers:connectors:sovity-dev").toBuilder() + .consumerConfigCustomizer(config -> config.getProperties() + .put("edc.flyway.additional.migration.locations", "classpath:db/additional-test-data/always-true-policy-migrated")) + .providerConfigCustomizer(config -> config.getProperties() + .put("edc.flyway.additional.migration.locations", "classpath:db/additional-test-data/always-true-policy-legacy")) + .build() + ); @Test @DisabledOnGithub diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index e55cc6f10..bc4d8bb10 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -37,7 +37,7 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @@ -52,6 +52,7 @@ import static de.sovity.edc.client.gen.model.ContractTerminatedBy.SELF; import static de.sovity.edc.client.gen.model.ContractTerminationStatus.ONGOING; import static de.sovity.edc.client.gen.model.ContractTerminationStatus.TERMINATED; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; import static java.time.Duration.ofSeconds; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -59,9 +60,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.DynamicTest.dynamicTest; -@ExtendWith(E2eTestExtension.class) public class ContractTerminationTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + @Test @DisabledOnGithub void canGetAgreementPageForNonTerminatedContract( diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java index f0bdd6ea3..300dd0fdc 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -55,7 +55,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @@ -77,6 +77,7 @@ import javax.annotation.Nullable; import static de.sovity.edc.client.gen.model.TransferProcessSimplifiedState.OK; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; import static java.time.Duration.ofSeconds; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; @@ -91,9 +92,12 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.stop.Stop.stopQuietly; -@ExtendWith(E2eTestExtension.class) + class DataSourceParameterizationTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + private final int port = getFreePort(); private final String sourcePath = "/source/some/path/"; private final String destinationPath = "/destination/some/path/"; diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java index 76b641959..b3bb3d0d5 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceQueryParamsTest.java @@ -28,15 +28,18 @@ import lombok.val; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.HashMap; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; -@ExtendWith(E2eTestExtension.class) class DataSourceQueryParamsTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + private MockDataAddressRemote dataAddress; private final String encodedParam = "a=%25"; // Unencoded param "a=%" diff --git a/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java b/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java index 08876aaa0..1f571485b 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ManagementApiTransferTest.java @@ -21,15 +21,18 @@ import de.sovity.edc.extension.e2e.extension.Provider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.util.UUID; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; -@ExtendWith(E2eTestExtension.class) class ManagementApiTransferTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + private MockDataAddressRemote dataAddress; private static final String TEST_BACKEND_TEST_DATA = UUID.randomUUID().toString(); diff --git a/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java b/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java index 5e6fb8b7f..c2d4b875c 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/PlaceholderDataSourceExtensionTest.java @@ -27,7 +27,7 @@ import lombok.SneakyThrows; import lombok.val; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @@ -37,12 +37,15 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; -@ExtendWith(E2eTestExtension.class) class PlaceholderDataSourceExtensionTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + @SneakyThrows @Test void shouldAccessDummyEndpoint( diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index 790e0c268..daae60d83 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -69,6 +69,7 @@ import static de.sovity.edc.client.gen.model.ContractAgreementDirection.CONSUMING; import static de.sovity.edc.client.gen.model.ContractAgreementDirection.PROVIDING; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -80,7 +81,7 @@ class UiApiWrapperTest { private static final String CONSUMER_PARTICIPANT_ID = "consumer"; @RegisterExtension - private static E2eTestExtension e2eTestExtension = new E2eTestExtension(CONSUMER_PARTICIPANT_ID, PROVIDER_PARTICIPANT_ID); + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); private MockDataAddressRemote dataAddress; diff --git a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java index 3231fda3d..521a670af 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UseCaseApiWrapperTest.java @@ -45,16 +45,19 @@ import de.sovity.edc.utils.jsonld.vocab.Prop; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import java.time.OffsetDateTime; import java.util.List; +import static de.sovity.edc.extension.e2e.extension.Helpers.defaultE2eTestExtension; import static org.assertj.core.api.Assertions.assertThat; -@ExtendWith(E2eTestExtension.class) class UseCaseApiWrapperTest { + @RegisterExtension + private static E2eTestExtension e2eTestExtension = defaultE2eTestExtension(); + private MockDataAddressRemote dataAddress; private final String dataOfferData = "expected data 123"; diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java index d344b5f00..495ba4a81 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/db/EdcRuntimeExtensionFixed.java @@ -49,6 +49,7 @@ * {@link AfterTestExecutionCallback} lifecycle hooks. Parameter injection of runtime services is supported. */ public class EdcRuntimeExtensionFixed extends EdcExtension { + private static final Monitor MONITOR = loadMonitor(); private final String moduleName; diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/CeE2eTestExtensionConfigFactory.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/CeE2eTestExtensionConfigFactory.java new file mode 100644 index 000000000..e096e2895 --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/CeE2eTestExtensionConfigFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class CeE2eTestExtensionConfigFactory { + + public static E2eTestExtensionConfig defaultBuilder() { + return E2eTestExtensionConfig.builder().moduleName(":launchers:connectors:sovity-dev").build(); + } + + public static E2eTestExtensionConfig withModule(String module) { + return defaultBuilder().toBuilder().moduleName(module).build(); + } +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java index 0605a69cd..54571fabd 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtension.java @@ -20,7 +20,6 @@ import de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfig; import de.sovity.edc.extension.e2e.db.EdcRuntimeExtensionWithTestDatabase; import de.sovity.edc.extension.utils.Lazy; -import lombok.Builder; import lombok.val; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.extension.AfterAllCallback; @@ -43,13 +42,11 @@ public class E2eTestExtension implements BeforeAllCallback, AfterAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { - private final String consumerParticipantId; - private final String additionalConsumerMigrationLocation; + private E2eTestExtensionConfig config; + private ConnectorConfig consumerConfig; private final EdcRuntimeExtensionWithTestDatabase consumerExtension; - private String providerParticipantId; - private final String additionalProviderMigrationLocation; private ConnectorConfig providerConfig; private final EdcRuntimeExtensionWithTestDatabase providerExtension; @@ -59,41 +56,35 @@ public class E2eTestExtension private Lazy clientAndServer; - public E2eTestExtension() { - this("consumer", "provider", "", ""); - } - - @Builder - public E2eTestExtension(String additionalConsumerMigrationLocation, String additionalProviderMigrationLocation) { - this("consumer", "provider", additionalConsumerMigrationLocation, additionalProviderMigrationLocation); + public E2eTestExtension(String moduleName) { + this(E2eTestExtensionConfig.builder().moduleName(moduleName).build()); } public E2eTestExtension( - String consumerParticipantId, - String providerParticipantId, - String additionalConsumerMigrationLocation, - String additionalProviderMigrationLocation + E2eTestExtensionConfig config ) { - this.consumerParticipantId = consumerParticipantId; - this.providerParticipantId = providerParticipantId; - this.additionalConsumerMigrationLocation = additionalConsumerMigrationLocation; - this.additionalProviderMigrationLocation = additionalProviderMigrationLocation; + this.config = config; consumerExtension = new EdcRuntimeExtensionWithTestDatabase( - ":launchers:connectors:sovity-dev", - "consumer", + config.getModuleName(), + config.getConsumerParticipantId(), testDatabase -> { - consumerConfig = forTestDatabase(this.consumerParticipantId, testDatabase); - consumerConfig.getProperties().put("edc.flyway.additional.migration.locations", this.additionalConsumerMigrationLocation); + consumerConfig = forTestDatabase(config.getConsumerParticipantId(), testDatabase); + + config.getConfigCustomizer().accept(consumerConfig); + config.getConsumerConfigCustomizer().accept(consumerConfig); return consumerConfig.getProperties(); } ); + providerExtension = new EdcRuntimeExtensionWithTestDatabase( - ":launchers:connectors:sovity-dev", - "provider", + config.getModuleName(), + config.getProviderParticipantId(), testDatabase -> { - providerConfig = forTestDatabase(this.providerParticipantId, testDatabase); - providerConfig.getProperties().put("edc.flyway.additional.migration.locations", this.additionalProviderMigrationLocation); + providerConfig = forTestDatabase(config.getProviderParticipantId(), testDatabase); + + config.getConfigCustomizer().accept(providerConfig); + config.getProviderConfigCustomizer().accept(providerConfig); return providerConfig.getProperties(); } ); @@ -171,7 +162,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } else if (ConnectorConfig.class.equals(type)) { return consumerConfig; } else if (ConnectorRemote.class.equals(type)) { - return newConnectorRemote(consumerParticipantId, consumerConfig); + return newConnectorRemote(config.getConsumerParticipantId(), consumerConfig); } else { return consumerExtension.resolveParameter(parameterContext, extensionContext); } @@ -183,7 +174,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } else if (ConnectorConfig.class.equals(type)) { return providerConfig; } else if (ConnectorRemote.class.equals(type)) { - return newConnectorRemote(providerParticipantId, providerConfig); + return newConnectorRemote(config.getProviderParticipantId(), providerConfig); } else { return providerExtension.resolveParameter(parameterContext, extensionContext); } diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtensionConfig.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtensionConfig.java new file mode 100644 index 000000000..1bb12cea1 --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eTestExtensionConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import lombok.Builder; +import lombok.Getter; + +import java.util.function.Consumer; + +@Builder(toBuilder = true) +@Getter +public class E2eTestExtensionConfig { + + private String moduleName; + + @Builder.Default + private String consumerParticipantId = "consumer"; + + @Builder.Default + private String providerParticipantId = "provider"; + + @Builder.Default + private Consumer configCustomizer = (it) -> { + }; + + @Builder.Default + private Consumer consumerConfigCustomizer = (it) -> { + }; + + @Builder.Default + private Consumer providerConfigCustomizer = (it) -> { + }; +} diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Helpers.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Helpers.java new file mode 100644 index 000000000..4fa3ae399 --- /dev/null +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/Helpers.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.e2e.extension; + +import lombok.experimental.UtilityClass; + +import static de.sovity.edc.extension.e2e.extension.CeE2eTestExtensionConfigFactory.withModule; + +@UtilityClass +public class Helpers { + public static E2eTestExtension defaultE2eTestExtension() { + return new E2eTestExtension(withModule(":launchers:connectors:sovity-dev")); + } +} From 727b9cfd2d2608ce48f2211f1b82269c17b1b8f7 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 1 Aug 2024 15:30:53 +0200 Subject: [PATCH 275/295] feat: fetch a single contract agreement by its ID (#1016) --------- Co-authored-by: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> --- CHANGELOG.md | 3 ++ docs/api/postman_collection.json | 29 ++++++++-- docs/api/sovity-edc-api-wrapper.yaml | 41 ++++++++++---- .../edc/ext/wrapper/api/ui/UiResource.java | 8 +++ .../ext/wrapper/api/ui/UiResourceImpl.java | 7 +++ .../ContractAgreementPageApiService.java | 14 ++++- .../ContractAgreementDataFetcher.java | 53 +++++++++++++++---- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 47 +++++++++++++++- .../extension/e2e/extension/E2eScenario.java | 4 ++ 9 files changed, 179 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ba7f037..5420b6380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor +- API Wrapper: + - Added wrapper API endpoint to query a single contract agreement + #### Patch #### Major Changes diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 21b2363a0..e857a8d83 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,7 +1,7 @@ { "info": { - "_postman_id": "14267003-be4b-40ee-a9b6-83b5dd32ba57", - "name": "sovity EDC Community Edition", + "_postman_id": "5f373580-db27-4daf-86e8-84bc8ed933c8", + "name": "sovity EDC Community Edition Copy", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "32949497" @@ -529,7 +529,7 @@ { "name": "Get Contract Agreements", "request": { - "method": "GET", + "method": "POST", "header": [], "url": { "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-agreement-page", @@ -546,6 +546,27 @@ }, "response": [] }, + { + "name": "Get Contract Agreements By ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{CONSUMER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/contract-agreement-page/{{CONTRACT_AGREEMENT_ID}}", + "host": [ + "{{CONSUMER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "contract-agreement-page", + "{{CONTRACT_AGREEMENT_ID}}" + ] + } + }, + "response": [] + }, { "name": "Terminate Contract Agreement", "event": [ @@ -2073,4 +2094,4 @@ "type": "default" } ] -} +} \ No newline at end of file diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 4440e71de..ef2167e31 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -268,6 +268,25 @@ paths: type: array items: $ref: '#/components/schemas/UiDataOffer' + /wrapper/ui/pages/contract-agreement-page/{contractAgreementId}: + get: + tags: + - UI + description: Get a single contract agreement card by its identifier + operationId: getContractAgreementCard + parameters: + - name: contractAgreementId + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/ContractAgreementCard' /wrapper/ui/pages/contract-agreement-page: post: tags: @@ -1436,17 +1455,6 @@ components: enum: - CONSUMING - PROVIDING - ContractAgreementPage: - required: - - contractAgreements - type: object - properties: - contractAgreements: - type: array - description: Contract Agreement Cards - items: - $ref: '#/components/schemas/ContractAgreementCard' - description: Data as required by the UI's Contract Agreement Page ContractAgreementTerminationInfo: required: - detail @@ -1528,6 +1536,17 @@ components: simplifiedState: $ref: '#/components/schemas/TransferProcessSimplifiedState' description: Transfer Process State interpreted + ContractAgreementPage: + required: + - contractAgreements + type: object + properties: + contractAgreements: + type: array + description: Contract Agreement Cards + items: + $ref: '#/components/schemas/ContractAgreementCard' + description: Data as required by the UI's Contract Agreement Page ContractAgreementPageQuery: type: object properties: diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 81013da0c..6268537df 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPageQuery; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; @@ -164,6 +165,13 @@ ContractAgreementPage getContractAgreementPage( @Nullable ContractAgreementPageQuery contractAgreementPageQuery ); + @GET + @Path("pages/contract-agreement-page/{contractAgreementId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Get a single contract agreement card by its identifier") + ContractAgreementCard getContractAgreementCard(@PathParam("contractAgreementId") String contractAgreementId); + @POST @Path("pages/contract-agreement-page/transfers") @Consumes(MediaType.APPLICATION_JSON) diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index ab856602f..53697eeff 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditRequest; import de.sovity.edc.ext.wrapper.api.ui.model.AssetPage; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementCard; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPage; import de.sovity.edc.ext.wrapper.api.ui.model.ContractAgreementPageQuery; import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionPage; @@ -155,6 +156,12 @@ public ContractAgreementPage getContractAgreementPage(@Nullable ContractAgreemen contractAgreementApiService.contractAgreementPage(dsl, contractAgreementPageQuery)); } + @Override + public ContractAgreementCard getContractAgreementCard(String contractAgreementId) { + return dslContextFactory.transactionResult(dsl -> + contractAgreementApiService.contractAgreement(dsl, contractAgreementId)); + } + @Override public IdResponseDto initiateTransfer(InitiateTransferRequest request) { return contractAgreementTransferApiService.initiateTransfer(request); diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java index 60a597b8b..f8cef441a 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/ContractAgreementPageApiService.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementDataFetcher; import de.sovity.edc.ext.wrapper.api.ui.pages.contract_agreements.services.ContractAgreementPageCardBuilder; import lombok.RequiredArgsConstructor; +import lombok.val; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jooq.DSLContext; @@ -48,9 +49,20 @@ public ContractAgreementPage contractAgreementPage(DSLContext dsl, @Nullable Con return new ContractAgreementPage(cards.toList()); } else { var filtered = cards.filter(card -> - card.getTerminationStatus().equals(contractAgreementPageQuery.getTerminationStatus())) + card.getTerminationStatus().equals(contractAgreementPageQuery.getTerminationStatus())) .toList(); return new ContractAgreementPage(filtered); } } + + public ContractAgreementCard contractAgreement(DSLContext dsl, String contractAgreementId) { + val agreementData = contractAgreementDataFetcher.getContractAgreement(dsl, contractAgreementId); + return contractAgreementPageCardBuilder.buildContractAgreementCard( + agreementData.agreement(), + agreementData.negotiation(), + agreementData.asset(), + agreementData.transfers(), + agreementData.termination() + ); + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java index a0f45c062..e0e9b1e14 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementDataFetcher.java @@ -18,6 +18,7 @@ import de.sovity.edc.ext.wrapper.api.ServiceException; import de.sovity.edc.ext.wrapper.utils.MapUtils; import lombok.RequiredArgsConstructor; +import lombok.val; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Map; +import java.util.function.Function; import static de.sovity.edc.ext.db.jooq.Tables.SOVITY_CONTRACT_TERMINATION; import static java.util.function.Function.identity; @@ -62,45 +64,78 @@ public List getContractAgreements(DSLContext dsl) { var transfers = getAllTransferProcesses().stream() .collect(groupingBy(it -> it.getDataRequest().getContractId())); - var terminations = fetchTerminations(dsl, agreements); + var agreementIds = agreements.stream().map(ContractAgreement::getId).toList(); + + var terminations = fetchTerminations(dsl, agreementIds); // A ContractAgreement has multiple ContractNegotiations when doing a loopback consumption return agreements.stream() .flatMap(agreement -> negotiations.getOrDefault(agreement.getId(), List.of()) .stream() .map(negotiation -> { - var asset = getAsset(agreement, negotiation, assets); + var asset = getAsset(agreement, negotiation, assets::get); var contractTransfers = transfers.getOrDefault(agreement.getId(), List.of()); return new ContractAgreementData(agreement, negotiation, asset, contractTransfers, terminations.get(agreement.getId())); })) .toList(); } - private @NotNull Map fetchTerminations(DSLContext dsl, List agreements) { + @NotNull + public ContractAgreementData getContractAgreement(DSLContext dsl, String contractAgreementId) { + val agreement = getContractAgreementById(contractAgreementId); - var agreementIds = agreements.stream().map(ContractAgreement::getId).toList(); + val negotiationQuery = QuerySpec.max(); + val negotiation = contractNegotiationStore.queryNegotiations(negotiationQuery) + .filter(it -> it.getContractAgreement().getId().equals(contractAgreementId)) + .findFirst() + .orElseThrow( + () -> new IllegalStateException("Can't find any negotiation for contract agreement id %s".formatted(contractAgreementId))); + + val transfers = getAllTransferProcesses().stream().collect(groupingBy(it -> it.getDataRequest().getContractId())); + + val terminations = fetchTerminations(dsl, agreement.getId()); + + val asset = getAsset(agreement, negotiation, (it) -> assetIndex.findById(agreement.getAssetId())); + + return new ContractAgreementData( + agreement, + negotiation, + asset, + transfers.getOrDefault(agreement.getId(), List.of()), + terminations.get(agreement.getId()) + ); + } + + private ContractAgreement getContractAgreementById(String id) { + return contractAgreementService.findById(id); + } + + @NotNull + private Map fetchTerminations(DSLContext dsl, String agreementIds) { + return fetchTerminations(dsl, List.of(agreementIds)); + } + @NotNull + private Map fetchTerminations(DSLContext dsl, List agreementIds) { var t = SOVITY_CONTRACT_TERMINATION; - var terminations = dsl.select() + return dsl.select() .from(t) .where(t.CONTRACT_AGREEMENT_ID.in(agreementIds)) .fetch() .into(t) .stream() .collect(toMap(SovityContractTerminationRecord::getContractAgreementId, identity())); - - return terminations; } - private Asset getAsset(ContractAgreement agreement, ContractNegotiation negotiation, Map assets) { + private Asset getAsset(ContractAgreement agreement, ContractNegotiation negotiation, Function selector) { var assetId = agreement.getAssetId(); if (negotiation.getType() == ContractNegotiation.Type.CONSUMER) { return dummyAsset(assetId); } - var asset = assets.get(assetId); + var asset = selector.apply(assetId); return asset == null ? dummyAsset(assetId) : asset; } diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index daae60d83..0e833b35b 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -531,7 +531,8 @@ void editAssetOnLiveContract( initiateTransfer(consumerClient, negotiation); // assert - assertThat(consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); + assertThat(consumerClient.uiApi().getCatalogPageDataOffers(providerProtocolEndpoint).get(0).getAsset().getTitle()) + .isEqualTo("Good Asset Title"); val firstAsset = providerClient.uiApi().getContractAgreementPage(null).getContractAgreements().get(0).getAsset(); assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" @@ -558,7 +559,8 @@ void editAssetOnLiveContract( """); validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); validateTransferProcessesOk(consumerClient, providerClient); - assertThat(providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0).getAssetName()).isEqualTo("Good Asset Title"); + assertThat(providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0).getAssetName()) + .isEqualTo("Good Asset Title"); } @Test @@ -589,6 +591,47 @@ void checkIdAvailability(E2eScenario scenario, @Provider EdcClient providerClien assertThat(posContractDefinitionResponse.getAvailable()).isTrue(); } + @Test + void retrieveSingleContractAgreement( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = scenario.createAsset(); + + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + // act + val retrieved = providerClient.uiApi().getContractAgreementCard(negotiation.getContractAgreementId()); + val alternative = providerClient.uiApi() + .getContractAgreementPage(null) + .getContractAgreements() + .stream() + .filter(it -> it.getContractAgreementId().equals(negotiation.getContractAgreementId())) + .findFirst() + .orElseThrow(); + + val retrievedPolicy = retrieved.getContractPolicy(); + val alternativePolicy = alternative.getContractPolicy(); + + retrieved.setContractPolicy(null); + alternative.setContractPolicy(null); + + // assert + assertThat(retrieved).usingRecursiveAssertion().isEqualTo(alternative); + + // assert separately because the policy ID is re-generated on each query + assertThat(retrievedPolicy) + .usingRecursiveComparison() + .ignoringFields("policyJsonLd") + .isEqualTo(alternativePolicy); + + assertThatJson(retrievedPolicy.getPolicyJsonLd()) + .whenIgnoringPaths("@id") + .isEqualTo(alternativePolicy.getPolicyJsonLd()); + } + private UiContractNegotiation negotiate( EdcClient consumerClient, ConnectorRemote consumerConnector, diff --git a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java index f54dc26b3..faafe0d28 100644 --- a/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java +++ b/utils/test-utils/src/main/java/de/sovity/edc/extension/e2e/extension/E2eScenario.java @@ -113,6 +113,10 @@ public String createAsset(String id, UiDataSource uiDataSource) { return internalCreateAsset(id, uiDataSource).getId(); } + public String createAsset(UiAssetCreateRequest uiAssetCreateRequest) { + return providerClient.uiApi().createAsset(uiAssetCreateRequest).getId(); + } + public MockedAsset createAssetWithMockResource(String id) { val path = "/assets/" + id; From 87add37fde585bd62323f30335f50880c82c23b3 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:13:35 +0200 Subject: [PATCH 276/295] chore(deps): update LoggingHouse dependency to latest version (#1015) --- CHANGELOG.md | 17 ++++++++++++----- .../goals/development/README.md | 4 ++-- .../deployment-guide/goals/local-demo/README.md | 4 ++-- .../deployment-guide/goals/production/README.md | 9 +++++++-- gradle/libs.versions.toml | 2 +- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5420b6380..ebe20ec07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,18 +9,25 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). ### Detailed Changes -#### Minor +#### Major Changes +#### Minor Changes - API Wrapper: - Added wrapper API endpoint to query a single contract agreement -#### Patch +#### Patch Changes +- Logginghouse-Client: Update logging-house-client extension to v1.1.0 -#### Major Changes +### Deployment Migration Notes +#### logging-house-client extension +As the updated logging-house-client extension now also saves data locally in a database, the following additional proerties must be added and set accordingly to a second additional database since the extension has its own flyway migration: +- ```EDC_DATASOURCE_LOGGINGHOUSE_URL```: "postgres://some-url" +- ```EDC_DATASOURCE_LOGGINGHOUSE_USER```: "username" +- ```EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD```: "password" -#### Minor Changes +If the extension is to be switched off, the following must now be set, as the extension is now activated by default when integrated: +- ```EDC_LOGGINGHOUSE_EXTENSION_ENABLED: 'false'``` -### Deployment Migration Notes #### Compatible Versions diff --git a/docs/deployment-guide/goals/development/README.md b/docs/deployment-guide/goals/development/README.md index 09239430c..f67884732 100644 --- a/docs/deployment-guide/goals/development/README.md +++ b/docs/deployment-guide/goals/development/README.md @@ -48,8 +48,8 @@ docker login ghcr.io # Pull the latest images docker compose --env-file .env.dev -f docker-compose-dev.yaml pull -# Start MDS EDC Connectors -EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose --env-file .env.dev -f docker-compose-dev.yaml up +# Start MDS EDC Connectors without activating the logging-house-extension +EDC_UI_ACTIVE_PROFILE=mds-open-source EDC_LOGGINGHOUSE_EXTENSION_ENABLED=false docker compose --env-file .env.dev -f docker-compose-dev.yaml up ``` diff --git a/docs/deployment-guide/goals/local-demo/README.md b/docs/deployment-guide/goals/local-demo/README.md index 7d15efd51..6675355f6 100644 --- a/docs/deployment-guide/goals/local-demo/README.md +++ b/docs/deployment-guide/goals/local-demo/README.md @@ -39,8 +39,8 @@ docker compose up # Log-In to the Github Container Registry docker login ghcr.io -# Start MDS EDC Connectors -EDC_UI_ACTIVE_PROFILE=mds-open-source docker compose up +# Start MDS EDC Connectors without activating the logging-house-extension +EDC_UI_ACTIVE_PROFILE=mds-open-source EDC_LOGGINGHOUSE_EXTENSION_ENABLED=false docker compose up ``` diff --git a/docs/deployment-guide/goals/production/README.md b/docs/deployment-guide/goals/production/README.md index b679811e6..849c7971c 100644 --- a/docs/deployment-guide/goals/production/README.md +++ b/docs/deployment-guide/goals/production/README.md @@ -29,7 +29,7 @@ To deploy an EDC multiple deployment units must be deployed and configured. |-------------------------------------------------------------------|---------------------------------------------------------------------------------------------| | An Auth Proxy / Auth solution of your choice. | (deployment specific, required to secure UI and management API) | | Reverse Proxy that merges multiple services and removes the ports | (deployment specific) | -| Postgresql | 13 or compatible version | +| Postgresql | 13 or compatible version, one for the EDC-data one for the LH-data | | EDC Backend | edc-ce or edc-ce-mds, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | | EDC UI | edc-ui, see [CHANGELOG.md](../../../../CHANGELOG.md) for compatible versions. | @@ -151,9 +151,14 @@ EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 A LoggingHouse extension is included in the MDS variant, which means that additional properties must be set for it: ```yaml -# LoggingHouse Extension +# LoggingHouse Extension general settings EDC_LOGGINGHOUSE_EXTENSION_ENABLED: "true" EDC_LOGGINGHOUSE_EXTENSION_URL: https://clearing.test.mobility-dataspace.eu + +# LoggingHouse Extension database connection for its own database +EDC_DATASOURCE_LOGGINGHOUSE_URL: jdbc:postgresql://postgresql2:5432/edc +EDC_DATASOURCE_LOGGINGHOUSE_USER: edc +EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD: edc ``` You can also optionally set the following config properties: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07d7ab5ed..f68253599 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ json = "20220924" jsonAssert = "1.5.1" jsonUnit = "3.2.7" junit = "5.10.0" -loggingHouse = "0.2.10" +loggingHouse = "v1.1.0" lombok = "1.18.30" mockito = "5.12.0" mockserver = "5.15.0" From 303860e8f421f3738deb68ed04f922a10dc1be17 Mon Sep 17 00:00:00 2001 From: Ilia Orlov <66363651+illfixit@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:23:32 +0200 Subject: [PATCH 277/295] New Always True policy should not cause errors (#1012) --- CHANGELOG.md | 2 ++ .../wrapper/api/common/mappers/policy/PolicyValidator.java | 4 ---- .../api/common/mappers/policy/PolicyValidatorTest.java | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe20ec07..44904d8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes - Logginghouse-Client: Update logging-house-client extension to v1.1.0 +- EDC Backend + - Fixed unrestricted policy wrongly displaying error ### Deployment Migration Notes #### logging-house-client extension diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java index 92b24cfad..743781dbe 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidator.java @@ -76,10 +76,6 @@ public void validateOtherPermissionFieldsUnset(Permission permission, MappingErr return; } - if (CollectionUtils.isEmpty(permission.getConstraints())) { - errors.add("Permission has no constraints."); - } - if (CollectionUtils.isNotEmpty(permission.getDuties())) { errors.add("Permission has duties, which is currently unsupported."); } diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java index 2151874d6..829d10c52 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/policy/PolicyValidatorTest.java @@ -174,7 +174,6 @@ void testPermission_full() { // assert assertThat(errors.getErrors()).containsExactlyInAnyOrder( - "$: Permission has no constraints.", "$: Permission has duties, which is currently unsupported.", "$: Permission has an assigner, which is currently unsupported.", "$: Permission has an assignee, which is currently unsupported.", From 37aff081a6af61df63b061692efd9cef617ea26c Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:04:30 +0200 Subject: [PATCH 278/295] chore: sync ap migrations (#1021) --- docs/api/sovity-edc-api-wrapper.yaml | 4 ++-- .../migration/V10__White_Label_Refactor.sql | 14 ++++++++++++++ .../edc/ext/catalog/crawler/utils/TestData.java | 4 ++-- .../crawler/dao/connectors/ConnectorQueries.java | 4 ++-- .../sovity/edc/ext/catalog/crawler/TestData.java | 4 ++-- 5 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V10__White_Label_Refactor.sql diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index ef2167e31..638cf6556 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -603,11 +603,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource + default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM - default: CUSTOM SecretValue: type: object properties: @@ -815,6 +815,7 @@ components: UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource + default: GET enum: - GET - POST @@ -822,7 +823,6 @@ components: - PATCH - DELETE - OPTIONS - default: GET UiDataSourceOnRequest: required: - contactEmail diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V10__White_Label_Refactor.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V10__White_Label_Refactor.sql new file mode 100644 index 000000000..605ddb335 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V10__White_Label_Refactor.sql @@ -0,0 +1,14 @@ +alter table organization + rename column mds_id to id; + +alter table "user" + rename column organization_mds_id to organization_id; + +alter table connector + rename column mds_id to organization_id; + +alter table connector + rename column provider_mds_id to provider_organization_id; + +alter table component + rename column mds_id to organization_id; diff --git a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java index 77e056a98..dae2d3f36 100644 --- a/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java +++ b/extensions/catalog-crawler/catalog-crawler-e2e-test/src/test/java/de/sovity/edc/ext/catalog/crawler/utils/TestData.java @@ -35,13 +35,13 @@ public static void insertConnector( Consumer applier ) { var organization = dsl.newRecord(Tables.ORGANIZATION); - organization.setMdsId(connectorRef.getOrganizationId()); + organization.setId(connectorRef.getOrganizationId()); organization.setName(connectorRef.getOrganizationLegalName()); organization.insert(); var connector = dsl.newRecord(Tables.CONNECTOR); connector.setEnvironment(connectorRef.getEnvironmentId()); - connector.setMdsId(connectorRef.getOrganizationId()); + connector.setOrganizationId(connectorRef.getOrganizationId()); connector.setConnectorId(connectorRef.getConnectorId()); connector.setName(connectorRef.getConnectorId() + " Name"); connector.setEndpointUrl(connectorRef.getEndpoint()); diff --git a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java index 4c3e3843b..b77a1b708 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java +++ b/extensions/catalog-crawler/catalog-crawler/src/main/java/de/sovity/edc/ext/catalog/crawler/dao/connectors/ConnectorQueries.java @@ -60,11 +60,11 @@ private Set queryConnectorRefs( c.CONNECTOR_ID.as("connectorId"), c.ENVIRONMENT.as("environmentId"), o.NAME.as("organizationLegalName"), - o.MDS_ID.as("organizationId"), + o.ID.as("organizationId"), c.ENDPOINT_URL.as("endpoint") ) .from(c) - .leftJoin(o).on(c.MDS_ID.eq(o.MDS_ID)) + .leftJoin(o).on(c.ORGANIZATION_ID.eq(o.ID)) .where(condition.apply(c, o), c.ENVIRONMENT.eq(crawlerConfig.getEnvironmentId())) .fetchInto(ConnectorRef.class); diff --git a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java index b522c8881..102beb54c 100644 --- a/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java +++ b/extensions/catalog-crawler/catalog-crawler/src/test/java/de/sovity/edc/ext/catalog/crawler/TestData.java @@ -44,13 +44,13 @@ public static void insertConnector( Consumer applier ) { var organization = dsl.newRecord(Tables.ORGANIZATION); - organization.setMdsId(connectorRef.getOrganizationId()); + organization.setId(connectorRef.getOrganizationId()); organization.setName(connectorRef.getOrganizationLegalName()); organization.insert(); var connector = dsl.newRecord(Tables.CONNECTOR); connector.setEnvironment(connectorRef.getEnvironmentId()); - connector.setMdsId(connectorRef.getOrganizationId()); + connector.setOrganizationId(connectorRef.getOrganizationId()); connector.setConnectorId(connectorRef.getConnectorId()); connector.setName(connectorRef.getConnectorId() + " Name"); connector.setEndpointUrl(connectorRef.getEndpoint()); From ba62af5be412ce083c52f1be2e770e73087f6671 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Fri, 9 Aug 2024 10:45:27 +0200 Subject: [PATCH 279/295] Release prep (#1022) --- .env | 6 ++--- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/.env b/.env index ef3c40517..97a3071f8 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:10.0.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.0.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.0 +EDC_IMAGE=ghcr.io/sovity/edc-dev:10.1.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.1.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.1 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/CHANGELOG.md b/CHANGELOG.md index 44904d8d1..3ab2c06b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,34 +12,72 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Major Changes #### Minor Changes + +#### Patch Changes + +### Deployment Migration Notes + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:{{ VERSION }}` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:{{ VERSION }}` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:{{ VERSION }}` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:{{ VERSION }}` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` + + +## [10.1.0] - 2024-08-09 + +### Overview + +MDS 2.2 patch release + +### Detailed Changes + +API Wrapper update and bug fixing. + +#### Minor Changes + - API Wrapper: - Added wrapper API endpoint to query a single contract agreement #### Patch Changes + - Logginghouse-Client: Update logging-house-client extension to v1.1.0 - EDC Backend - Fixed unrestricted policy wrongly displaying error + - Performance improvement when fetching a single contract agreement +- EDC UI + - Copyable contact email and subject fields on asset and data offer detail dialogs + - Assets Page search input field is now case-insensitive + - Markdown support for Reference files description, Conditions for use fields + - Fixed wrong date format when creating a new data offer + - Temporarily re-implemented the Create Asset Dialog ### Deployment Migration Notes + #### logging-house-client extension -As the updated logging-house-client extension now also saves data locally in a database, the following additional proerties must be added and set accordingly to a second additional database since the extension has its own flyway migration: -- ```EDC_DATASOURCE_LOGGINGHOUSE_URL```: "postgres://some-url" -- ```EDC_DATASOURCE_LOGGINGHOUSE_USER```: "username" -- ```EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD```: "password" -If the extension is to be switched off, the following must now be set, as the extension is now activated by default when integrated: -- ```EDC_LOGGINGHOUSE_EXTENSION_ENABLED: 'false'``` +As the updated logging-house-client extension now also saves data locally in a database, the following additional properties must be added and set accordingly to a second additional database since the extension has its own flyway migration: +- `EDC_DATASOURCE_LOGGINGHOUSE_URL`: "postgres://some-url" +- `EDC_DATASOURCE_LOGGINGHOUSE_USER`: "username" +- `EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD`: "password" + +If the extension is to be switched off, the following must now be set, as the extension is now activated by default when integrated: +- `EDC_LOGGINGHOUSE_EXTENSION_ENABLED: 'false'` #### Compatible Versions - Connector Backend Docker Images: - - Dev EDC: `ghcr.io/sovity/edc-dev:{{ VERSION }}` - - sovity EDC CE: `ghcr.io/sovity/edc-ce:{{ VERSION }}` - - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:{{ VERSION }}` - - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:{{ VERSION }}` - - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` -- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` + - Dev EDC: `ghcr.io/sovity/edc-dev:10.1.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.1.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.1.0` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.1.0` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.1.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.1` ## [10.0.0] - 2024-07-24 From 1c24116a43e9ee8fc95b42ca3e6e7b565fb87063 Mon Sep 17 00:00:00 2001 From: Julian Jarminowski Date: Tue, 13 Aug 2024 16:54:42 +0200 Subject: [PATCH 280/295] fix: Remove duplicate database indices (#1024) * fix: Remove duplicate database indices * fix: Improve documentation --- CHANGELOG.md | 2 ++ .../migration/V12__DROP_duplicate_indices.sql | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 extensions/postgres-flyway/src/main/resources/db/migration/V12__DROP_duplicate_indices.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab2c06b5..09e0442c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes +- Improve database performance by removing duplicate indexes + ### Deployment Migration Notes #### Compatible Versions diff --git a/extensions/postgres-flyway/src/main/resources/db/migration/V12__DROP_duplicate_indices.sql b/extensions/postgres-flyway/src/main/resources/db/migration/V12__DROP_duplicate_indices.sql new file mode 100644 index 000000000..e1a8f3d46 --- /dev/null +++ b/extensions/postgres-flyway/src/main/resources/db/migration/V12__DROP_duplicate_indices.sql @@ -0,0 +1,21 @@ +-- +-- Copyright (c) 2024 sovity GmbH +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- sovity GmbH - Improve database performance by removing duplicate indices +-- + +-- Drop the duplicate indexes if they exist for improved resource usage. +DROP INDEX IF EXISTS contract_agreement_id_uindex; +DROP INDEX IF EXISTS contract_negotiation_id_uindex; +DROP INDEX IF EXISTS data_request_id_uindex; +DROP INDEX IF EXISTS lease_lease_id_uindex; +DROP INDEX IF EXISTS edc_policydefinitions_id_uindex; +DROP INDEX IF EXISTS transfer_process_id_uindex; + From 1aa1e1817bbadb6a4fbfd74794c5459a2272b2ca Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:01:40 +0200 Subject: [PATCH 281/295] docs: add faq.md (#1026) --- docs/getting-started/documentation/faq.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/getting-started/documentation/faq.md diff --git a/docs/getting-started/documentation/faq.md b/docs/getting-started/documentation/faq.md new file mode 100644 index 000000000..995788d8c --- /dev/null +++ b/docs/getting-started/documentation/faq.md @@ -0,0 +1,13 @@ +Welcome to our Frequently Asked Questions (FAQ) collection! +======== + +This section is designed to provide quick and clear answers to the most common queries we receive. Whether you're looking for information on our products, services, policies, or need help with troubleshooting, you'll find the answers here. +If you can't find the answer to your question, don't hesitate to open a [dicussion](https://github.com/sovity/edc-ce/discussions). + +### How does the EDC API-key work for backend and frontend for API-authentication? + +**Backend (EDC):** +The variable ```EDC_API_AUTH_KEY``` in the EDC backend is used to define the API-key for the Management-API and API-Wrapper in the first place. External requests from external services to the EDC backend and its Management-API/API-Wrapper are then authenticated against this value and the requests must contain this API-key accordingly. + +**Frontend (EDC UI):** +The frontend is such an external service whose API requests to the Management-API/API-Wrapper of the EDC backend must be authenticated like any other API request, hence the variable ```EDC_UI_MANAGEMENT_API_KEY``` in the EDC UI. It therefore tells the UI which API-key it should use for its own requests to the EDC backend in order to be successfully authenticated there, so that the UI can query and display data from the EDC backend or create assets etc. From 9b6ec3ff82560162c378b098df0adade24969a49 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Mon, 19 Aug 2024 17:41:22 +0200 Subject: [PATCH 282/295] fix: Address update (#1029) --------- Co-authored-by: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/api/sovity-edc-api-wrapper.yaml | 4 +- .../api/ui/pages/asset/AssetApiService.java | 1 + .../de/sovity/edc/e2e/UiApiWrapperTest.java | 44 +++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e0442c2..ee5cfc851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes - Improve database performance by removing duplicate indexes +- The data address is now correctly updated when editing an asset. ### Deployment Migration Notes diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index 638cf6556..ef2167e31 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -603,11 +603,11 @@ components: DataSourceType: type: string description: Supported Data Source Types by UiDataSource - default: CUSTOM enum: - HTTP_DATA - ON_REQUEST - CUSTOM + default: CUSTOM SecretValue: type: object properties: @@ -815,7 +815,6 @@ components: UiDataSourceHttpDataMethod: type: string description: Supported HTTP Methods by UiDataSource - default: GET enum: - GET - POST @@ -823,6 +822,7 @@ components: - PATCH - DELETE - OPTIONS + default: GET UiDataSourceOnRequest: required: - contactEmail diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java index d4ba7ea02..d2266f393 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java @@ -62,6 +62,7 @@ public IdResponseDto editAsset(String assetId, UiAssetEditRequest request) { Objects.requireNonNull(foundAsset, "Asset with ID %s not found".formatted(assetId)); val editedAsset = assetMapper.editAsset(foundAsset, request); val updatedAsset = assetService.update(editedAsset).orElseThrow(ServiceException::new); + assetService.update(editedAsset.getId(), editedAsset.getDataAddress()); return new IdResponseDto(updatedAsset.getId()); } diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index 0e833b35b..19dd2cbc1 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -18,6 +18,7 @@ import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataSourceAvailability; import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; @@ -35,6 +36,8 @@ import de.sovity.edc.client.gen.model.UiDataOffer; import de.sovity.edc.client.gen.model.UiDataSource; import de.sovity.edc.client.gen.model.UiDataSourceHttpData; +import de.sovity.edc.client.gen.model.UiDataSourceHttpDataMethod; +import de.sovity.edc.client.gen.model.UiDataSourceOnRequest; import de.sovity.edc.client.gen.model.UiPolicyConstraint; import de.sovity.edc.client.gen.model.UiPolicyExpression; import de.sovity.edc.client.gen.model.UiPolicyExpressionType; @@ -591,6 +594,7 @@ void checkIdAvailability(E2eScenario scenario, @Provider EdcClient providerClien assertThat(posContractDefinitionResponse.getAvailable()).isTrue(); } + @DisabledOnGithub @Test void retrieveSingleContractAgreement( E2eScenario scenario, @@ -632,6 +636,46 @@ void retrieveSingleContractAgreement( .isEqualTo(alternativePolicy.getPolicyJsonLd()); } + @Test + void canMakeAnOnDemandDataSourceAvailable( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = scenario.createAsset(UiAssetCreateRequest.builder() + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("whatever@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .id("asset") + .title("foo") + .build()); + + // act + + providerClient.uiApi().editAsset(assetId, UiAssetEditRequest.builder() + .dataSourceOverrideOrNull(UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .method(UiDataSourceHttpDataMethod.GET) + .baseUrl("http://example.com/baseUrl") + .build()) + .build()) + .build()); + + val asset = + providerClient.uiApi().getAssetPage().getAssets().stream().filter(it -> it.getAssetId().equals(assetId)).findFirst().get(); + + // assert + assertThat(asset.getDataSourceAvailability()).isEqualTo(DataSourceAvailability.LIVE); + assertThatJson(asset.getAssetJsonLd()) + .inPath("$.[\"https://w3id.org/edc/v0.0.1/ns/dataAddress\"][\"https://w3id.org/edc/v0.0.1/ns/baseUrl\"]") + .isEqualTo("\"http://example.com/baseUrl\""); + } + private UiContractNegotiation negotiate( EdcClient consumerClient, ConnectorRemote consumerConnector, From 8af8ff885875ccc5b0fedb6d62b67e90be9dd3c0 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Tue, 20 Aug 2024 10:39:50 +0200 Subject: [PATCH 283/295] fix: flyway validation failing in mds variant (#1025) --- CHANGELOG.md | 8 ++++++++ .../de/sovity/edc/extension/postgresql/FlywayFactory.java | 2 ++ launchers/.env.connector | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5cfc851..7167f23e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,17 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). - Improve database performance by removing duplicate indexes - The data address is now correctly updated when editing an asset. +- Fix a database initialization error when starting the EDC with Logging House v1.1.0 ### Deployment Migration Notes +#### MDS only + +##### logging-house-client extension + +If the extension is to be switched off, the following must now be set, as the extension is now activated by default when integrated: +- `EDC_LOGGINGHOUSE_EXTENSION_ENABLED: 'false'` + #### Compatible Versions - Connector Backend Docker Images: diff --git a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java index a742381be..636983e70 100644 --- a/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java +++ b/extensions/postgres-flyway/src/main/java/de/sovity/edc/extension/postgresql/FlywayFactory.java @@ -39,6 +39,8 @@ public Flyway setupFlywayForUnifiedHistory(DataSource dataSource) { return Flyway.configure() .dataSource(dataSource) .cleanDisabled(!config.flywayCleanEnabled()) + .baselineVersion("0") + .baselineOnMigrate(true) .table("flyway_schema_history") .locations(locations.toArray(new String[0])) .load(); diff --git a/launchers/.env.connector b/launchers/.env.connector index 5b2eb4284..e589980c3 100644 --- a/launchers/.env.connector +++ b/launchers/.env.connector @@ -99,3 +99,8 @@ EDC_VAULT=/app/empty-properties-file.properties # Base URL for the On Request asset datasource, as reachable by the data plane MY_EDC_DATASOURCE_PLACEHOLDER_BASEURL=${EDC_DSP_CALLBACK_ADDRESS} + +# Make the Logging House use the same DB as the EDC +EDC_DATASOURCE_LOGGINGHOUSE_URL=${MY_EDC_JDBC_URL} +EDC_DATASOURCE_LOGGINGHOUSE_USER=${MY_EDC_JDBC_USER} +EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD=${MY_EDC_JDBC_PASSWORD} From df510db69ee01c7b3c257afebc4f93ea61e5e5c0 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Tue, 20 Aug 2024 15:59:26 +0200 Subject: [PATCH 284/295] chore: Release 10.2.0 preparation (#1032) --- .env | 6 +- CHANGELOG.md | 62 ++++++++++--------- .../goals/production/README.md | 2 +- gradle/libs.versions.toml | 2 +- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/.env b/.env index 97a3071f8..816b50f87 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:10.1.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.1.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.1 +EDC_IMAGE=ghcr.io/sovity/edc-dev:10.2.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.2.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.2 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/CHANGELOG.md b/CHANGELOG.md index 7167f23e3..202c865d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,19 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes -- Improve database performance by removing duplicate indexes -- The data address is now correctly updated when editing an asset. -- Fix a database initialization error when starting the EDC with Logging House v1.1.0 - ### Deployment Migration Notes -#### MDS only - -##### logging-house-client extension - -If the extension is to be switched off, the following must now be set, as the extension is now activated by default when integrated: -- `EDC_LOGGINGHOUSE_EXTENSION_ENABLED: 'false'` - #### Compatible Versions - Connector Backend Docker Images: @@ -38,57 +27,70 @@ If the extension is to be switched off, the following must now be set, as the ex - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` - -## [10.1.0] - 2024-08-09 +## [10.2.0] - 2024-08-20 ### Overview -MDS 2.2 patch release +API Wrapper update, several bug fixes and database performance improvements. ### Detailed Changes -API Wrapper update and bug fixing. +This is a replacement for redacted release `10.1.0` with a few additional bug fixes. #### Minor Changes - API Wrapper: - - Added wrapper API endpoint to query a single contract agreement + - Added wrapper API endpoint to query a single contract agreement #### Patch Changes +- Core EDC + - Improve database performance by removing duplicate indexes and using UUID version 7. - Logginghouse-Client: Update logging-house-client extension to v1.1.0 - EDC Backend - - Fixed unrestricted policy wrongly displaying error - - Performance improvement when fetching a single contract agreement + - Fixed unrestricted policy wrongly displaying error + - Performance improvement when fetching a single contract agreement + - The data address is now correctly updated when editing an asset + - Fix a database initialization error when starting the EDC with Logging House v1.1.0 - EDC UI - Copyable contact email and subject fields on asset and data offer detail dialogs - Assets Page search input field is now case-insensitive - Markdown support for Reference files description, Conditions for use fields - Fixed wrong date format when creating a new data offer - Temporarily re-implemented the Create Asset Dialog + - Added description for fields in asset creation mask + - Added proper handling of custom JSON properties in edit asset process ### Deployment Migration Notes -#### logging-house-client extension +#### MDS only -As the updated logging-house-client extension now also saves data locally in a database, the following additional properties must be added and set accordingly to a second additional database since the extension has its own flyway migration: -- `EDC_DATASOURCE_LOGGINGHOUSE_URL`: "postgres://some-url" -- `EDC_DATASOURCE_LOGGINGHOUSE_USER`: "username" -- `EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD`: "password" +##### logging-house-client extension If the extension is to be switched off, the following must now be set, as the extension is now activated by default when integrated: - `EDC_LOGGINGHOUSE_EXTENSION_ENABLED: 'false'` - #### Compatible Versions - Connector Backend Docker Images: - - Dev EDC: `ghcr.io/sovity/edc-dev:10.1.0` - - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.1.0` - - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.1.0` - - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.1.0` - - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.1.0` -- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.1` + - Dev EDC: `ghcr.io/sovity/edc-dev:10.2.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.2.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.2.0` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.2.0` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.2.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.2` + +## [10.1.0] - 2024-08-09 + +### Overview + +MDS 2.2 patch release + +*Redacted* + +This release contained a major bug that prevented the EDC from starting when the logging house and the EDC shared the same database. + +This was fixed in 10.2.0 ## [10.0.0] - 2024-07-24 diff --git a/docs/deployment-guide/goals/production/README.md b/docs/deployment-guide/goals/production/README.md index 849c7971c..88d1d0c80 100644 --- a/docs/deployment-guide/goals/production/README.md +++ b/docs/deployment-guide/goals/production/README.md @@ -155,7 +155,7 @@ A LoggingHouse extension is included in the MDS variant, which means that additi EDC_LOGGINGHOUSE_EXTENSION_ENABLED: "true" EDC_LOGGINGHOUSE_EXTENSION_URL: https://clearing.test.mobility-dataspace.eu -# LoggingHouse Extension database connection for its own database +# Optional: LoggingHouse Extension database connection if you don't want to share the database between the EDC and the Logging House. By default, both the LH and the EDC share the same database. EDC_DATASOURCE_LOGGINGHOUSE_URL: jdbc:postgresql://postgresql2:5432/edc EDC_DATASOURCE_LOGGINGHOUSE_USER: edc EDC_DATASOURCE_LOGGINGHOUSE_PASSWORD: edc diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f68253599..82f6b2cad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ commonsCompress = "1.26.1" commonsCollections = "4.4" commonsIo = "2.13.0" commonsLang = "3.13.0" -edc = "0.2.1.3" +edc = "0.2.1.4" findbugs = "3.0.2" flexmark = "0.64.8" flyway = "9.0.1" From a450640eb172519a09c2112dabf87ce3586fccf2 Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:52:01 +0200 Subject: [PATCH 285/295] docs: add on-request data-source as example in asset-creation (#1034) --- docs/api/postman_collection.json | 36 +++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index e857a8d83..f557a0b0f 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "5f373580-db27-4daf-86e8-84bc8ed933c8", + "_postman_id": "3e0d6241-260c-42d3-81a1-3e3d4e9497e1", "name": "sovity EDC Community Edition Copy", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "32949497" + "_exporter_id": "31514741" }, "item": [ { @@ -107,6 +107,36 @@ }, "response": [] }, + { + "name": "Create Asset (with data-source on-request)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"testname-v1.0\",\r\n \"title\": \"TestName\",\r\n \"language\": \"https://w3id.org/idsa/code/EN\",\r\n \"description\": \"Testdescription\",\r\n \"publisherHomepage\": \"https://www.sovity.de\",\r\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\r\n \"version\": \"v1.0\",\r\n \"keywords\": [\r\n \"keyword1\",\r\n \"keyword2\"\r\n ],\r\n \"mediaType\": \"application/json\",\r\n \"landingPageUrl\": \"https://www.google.com\",\r\n \"dataAddressProperties\": {\r\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\r\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\r\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\r\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\r\n },\r\n \"dataSource\": {\r\n \"type\": \"ON_REQUEST\",\r\n \"onRequest\": {\r\n \"contactEmail\": \"contact@sovity.de\",\r\n \"contactPreferredEmailSubject\": \"This asset is created just for testing purposes\"\r\n }\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "asset-page", + "assets" + ] + } + }, + "response": [] + }, { "name": "Create Asset (with paramterization)", "request": { @@ -2094,4 +2124,4 @@ "type": "default" } ] -} \ No newline at end of file +} From 180332b124cb22680bf11ce6478599ff8dc2967e Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:09:08 +0200 Subject: [PATCH 286/295] fix(postman): update collection to reflect recent API changes (#1037) --- docs/api/postman_collection.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index f557a0b0f..0a7e8e50a 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3e0d6241-260c-42d3-81a1-3e3d4e9497e1", + "_postman_id": "213f5ff6-e5c6-41bb-9202-8e6cf60fec0c", "name": "sovity EDC Community Edition Copy", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -212,7 +212,7 @@ } }, "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets/{{ASSET_ID}}/metadata", + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/asset-page/assets/{{ASSET_ID}}", "host": [ "{{PROVIDER_EDC_MANAGEMENT_URL}}" ], @@ -222,8 +222,7 @@ "pages", "asset-page", "assets", - "{{ASSET_ID}}", - "metadata" + "{{ASSET_ID}}" ], "query": [ { @@ -2124,4 +2123,4 @@ "type": "default" } ] -} +} \ No newline at end of file From 9a5df5045f5f3272c79c0fbe0f14dca905e865b1 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 4 Sep 2024 11:21:03 +0200 Subject: [PATCH 287/295] feat: log terminations to logging house (#1027) --- CHANGELOG.md | 4 + build.gradle.kts | 2 +- .../ContractAgreementTerminationService.java | 42 +++++- .../ContractTerminationEvent.java | 35 +++++ .../ContractTerminationExtension.java | 2 +- .../ContractTerminationObserver.java | 50 +++++++ .../query/TerminateContractQuery.java | 4 +- extensions/mds-logginghouse-binder/README.md | 44 ++++++ .../mds-logginghouse-binder/build.gradle.kts | 62 +++++++++ .../mdslogginhousebinder/LogEntry.java | 56 ++++++++ .../LoggingHouseException.java | 21 +++ .../MdsContractTerminationEvent.java | 27 ++++ .../MdsContractTerminationObserver.java | 81 +++++++++++ .../MdsLoggingHouseBinder.java | 50 +++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + gradle/libs.versions.toml | 2 +- launchers/common/base-mds/build.gradle.kts | 1 + settings.gradle.kts | 1 + .../e2e/AlwaysTrueMigrationReversedTest.java | 14 ++ .../edc/e2e/ContractTerminationTest.java | 127 +++++++++++++++--- 20 files changed, 598 insertions(+), 28 deletions(-) create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java create mode 100644 extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java create mode 100644 extensions/mds-logginghouse-binder/README.md create mode 100644 extensions/mds-logginghouse-binder/build.gradle.kts create mode 100644 extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java create mode 100644 extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java create mode 100644 extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java create mode 100644 extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java create mode 100644 extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java create mode 100644 extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension diff --git a/CHANGELOG.md b/CHANGELOG.md index 202c865d3..6ff6d81d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Add Contract Termination Observer +- MDS only + - Log contract termination events in the logging house + #### Patch Changes ### Deployment Migration Notes diff --git a/build.gradle.kts b/build.gradle.kts index 9ee6fa69e..d8cb980a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -129,7 +129,7 @@ subprojects { tasks.register("printClasspath") { group = libs.versions.edcGroup.get() description = "The EdcRuntimeExtension JUnit Extension requires the gradle task 'printClasspath'" - println(sourceSets.main.get().runtimeClasspath.asPath); + println(sourceSets.main.get().runtimeClasspath.asPath) } java { diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java index 36a151b5b..a8c8f7bf7 100644 --- a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java @@ -16,15 +16,20 @@ import de.sovity.edc.extension.contacttermination.query.ContractAgreementTerminationDetailsQuery; import de.sovity.edc.extension.contacttermination.query.TerminateContractQuery; +import de.sovity.edc.extension.messenger.SovityMessage; import de.sovity.edc.extension.messenger.SovityMessenger; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.val; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.observe.Observable; +import org.eclipse.edc.spi.observe.ObservableImpl; import org.jetbrains.annotations.Nullable; import org.jooq.DSLContext; import java.time.OffsetDateTime; +import java.util.function.Consumer; import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.COUNTERPARTY; import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.SELF; @@ -37,15 +42,20 @@ public class ContractAgreementTerminationService { private final TerminateContractQuery terminateContractQuery; private final Monitor monitor; private final String thisParticipantId; + @Getter + private final Observable contractTerminationObservable = new ObservableImpl<>(); /** * This is to terminate an EDC's own contract. * If the termination comes from an external system, use - * {@link #terminateCounterpartyAgreement(DSLContext, String, ContractTerminationParam)} + * {@link #terminateAgreementAsCounterparty(DSLContext, String, ContractTerminationParam)} * to validate the counter-party's identity. */ public OffsetDateTime terminateAgreementOrThrow(DSLContext dsl, ContractTerminationParam termination) { + val starterEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), thisParticipantId); + notifyObservers(it -> it.contractTerminationStartedFromThisInstance(starterEvent)); + val details = contractAgreementTerminationDetailsQuery.fetchAgreementDetailsOrThrow(dsl, termination.contractAgreementId()); if (details == null) { @@ -58,16 +68,22 @@ public OffsetDateTime terminateAgreementOrThrow(DSLContext dsl, ContractTerminat val terminatedAt = terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, SELF); + val endEvent = ContractTerminationEvent.from(termination, terminatedAt, thisParticipantId); + notifyObservers(it -> it.contractTerminationCompletedOnThisInstance(endEvent)); + notifyTerminationToProvider(details.counterpartyAddress(), termination); return terminatedAt; } - public OffsetDateTime terminateCounterpartyAgreement( + public OffsetDateTime terminateAgreementAsCounterparty( DSLContext dsl, @Nullable String identity, ContractTerminationParam termination ) { + val starterEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), null); + notifyObservers(it -> it.contractTerminatedByCounterpartyStarted(starterEvent)); + val details = contractAgreementTerminationDetailsQuery.fetchAgreementDetailsOrThrow(dsl, termination.contractAgreementId()); if (details == null) { @@ -90,15 +106,35 @@ public OffsetDateTime terminateCounterpartyAgreement( val agent = thisParticipantId.equals(details.counterpartyId()) ? SELF : COUNTERPARTY; - return terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, agent); + val result = terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, agent); + + val endEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), details.counterpartyId()); + notifyObservers(it -> it.contractTerminatedByCounterparty(endEvent)); + + return result; } public void notifyTerminationToProvider(String counterPartyAddress, ContractTerminationParam termination) { + + val notificationEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), null); + notifyObservers(it -> it.contractTerminationOnCounterpartyStarted(notificationEvent)); + sovityMessenger.send( + SovityMessage.class, counterPartyAddress, new ContractTerminationMessage( termination.contractAgreementId(), termination.detail(), termination.reason())); } + + private void notifyObservers(Consumer call) { + for (val listener : contractTerminationObservable.getListeners()) { + try { + call.accept(listener); + } catch (Exception e) { + monitor.warning("Failure when notifying the contract termination listener.", e); + } + } + } } diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java new file mode 100644 index 000000000..4def044dc --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import java.time.OffsetDateTime; + +public record ContractTerminationEvent( + String contractAgreementId, + String detail, + String reason, + OffsetDateTime timestamp, + String origin +) { + public static ContractTerminationEvent from(ContractTerminationParam contractTerminationParam, OffsetDateTime dateTime, String origin) { + return new ContractTerminationEvent( + contractTerminationParam.contractAgreementId(), + contractTerminationParam.detail(), + contractTerminationParam.reason(), + dateTime, + origin + ); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java index cad805be9..84bb39b0f 100644 --- a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java @@ -91,7 +91,7 @@ private void setupMessenger(ContractAgreementTerminationService terminationServi ContractTerminationMessage.class, (claims, termination) -> dslContextFactory.transactionResult(dsl -> - terminationService.terminateCounterpartyAgreement( + terminationService.terminateAgreementAsCounterparty( dsl, participantAgentService.createFor(claims).getIdentity(), buildTerminationRequest(termination)))); diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java new file mode 100644 index 000000000..ef9cee608 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +public interface ContractTerminationObserver { + + /** + * Indicates that a contract termination was started by this EDC. + */ + default void contractTerminationStartedFromThisInstance(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that the first step to terminate a contract, terminating a contract on this EDC instance itself, was successful. + * The contract is now marked as terminated on this EDC's side. + */ + default void contractTerminationCompletedOnThisInstance(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that a contract termination on the counterparty EDC was started. + */ + default void contractTerminationOnCounterpartyStarted(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that a contract termination was started by a counterparty EDC terminated successfully + */ + default void contractTerminatedByCounterpartyStarted(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that a contract termination initiated by a counterparty EDC terminated successfully + * The contract is now marked as terminated on this EDC. + */ + default void contractTerminatedByCounterparty(ContractTerminationEvent contractTerminationEvent) { + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java index f404eb92c..5873784b8 100644 --- a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java @@ -31,8 +31,8 @@ public class TerminateContractQuery { public OffsetDateTime terminateConsumerAgreementOrThrow( DSLContext dsl, ContractTerminationParam termination, - ContractTerminatedBy terminatedBy) { - + ContractTerminatedBy terminatedBy + ) { val tooAccurate = OffsetDateTime.now(); val now = tooAccurate.truncatedTo(ChronoUnit.MICROS); diff --git a/extensions/mds-logginghouse-binder/README.md b/extensions/mds-logginghouse-binder/README.md new file mode 100644 index 000000000..6330791e8 --- /dev/null +++ b/extensions/mds-logginghouse-binder/README.md @@ -0,0 +1,44 @@ + +
      +
      + + Logo + + +

      EDC-Connector Extension:
      MDS Contract Termination - LoggingHouse binder

      + +

      + Report Bug + · + Request Feature +

      +
      + + +## About this Extension + +It links the Contract Termination events with the LoggingHouse. + +## Why does this extension exist? + +MDS needs to log the events generated when terminating a contract with their Logging House extension. +The Logging House is an external dependency and the linkage must only happen for the MDS variant. + +This extension implements this specific task. + +## Architecture + +```mermaid +flowchart TD + Binder(MDS LoggingHouse Binder) --> LoggingHouse(Logging House Extension) + Binder(MDS LoggingHouse Binder) --> ContractTermination(Contract Termination Extension) + MDS(MDS CE) --> Binder +``` + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/mds-logginghouse-binder/build.gradle.kts b/extensions/mds-logginghouse-binder/build.gradle.kts new file mode 100644 index 000000000..bd378ea58 --- /dev/null +++ b/extensions/mds-logginghouse-binder/build.gradle.kts @@ -0,0 +1,62 @@ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + implementation(project(":extensions:contract-termination")) + + implementation(libs.edc.coreSpi) + implementation(libs.edc.dspNegotiationTransform) + implementation(libs.edc.transferSpi) + + implementation(libs.loggingHouse.client) + implementation(libs.jakarta.rsApi) + + + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + + testImplementation(project(":extensions:postgres-flyway")) + testImplementation(project(":utils:test-utils")) + testImplementation(project(":utils:versions")) + + testImplementation(libs.edc.monitorJdkLogger) + testImplementation(libs.edc.http) { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation(libs.bundles.jetty.cve2023) + + testImplementation(libs.assertj.core) + testImplementation(libs.flyway.core) + testImplementation(libs.junit.api) + testImplementation(libs.hibernate.validation) + testImplementation(libs.jakarta.el) + testImplementation(libs.mockito.core) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.testcontainers.postgresql) + + testRuntimeOnly(libs.junit.engine) +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java new file mode 100644 index 000000000..0ec722beb --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.contacttermination.ContractTerminationEvent; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; + +@AllArgsConstructor +@Builder +@Data +@NoArgsConstructor +public class LogEntry { + + @JsonProperty("contractAgreementId") + private String contractAgreementId; + + @JsonProperty("event") + private String event; + + @JsonProperty("detail") + private String detail; + + @JsonProperty("reason") + private String reason; + + @JsonProperty("timestamp") + private OffsetDateTime timestamp; + + public static LogEntry from(String event, ContractTerminationEvent contractTerminationEvent) { + return LogEntry.builder() + .event(event) + .contractAgreementId(contractTerminationEvent.contractAgreementId()) + .detail(contractTerminationEvent.detail()) + .reason(contractTerminationEvent.reason()) + .timestamp(contractTerminationEvent.timestamp()) + .build(); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java new file mode 100644 index 000000000..6ca7c24a1 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +public class LoggingHouseException extends RuntimeException { + public LoggingHouseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java new file mode 100644 index 000000000..0237f5fa2 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.truzzt.extension.logginghouse.client.events.CustomLoggingHouseEvent; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class MdsContractTerminationEvent extends CustomLoggingHouseEvent { + private final String eventId; + private final String processId; + private final String messageBody; +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java new file mode 100644 index 000000000..3ba115efb --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.contacttermination.ContractTerminationEvent; +import de.sovity.edc.extension.contacttermination.ContractTerminationObserver; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.uuid.UuidGenerator; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor +public class MdsContractTerminationObserver implements ContractTerminationObserver { + + private final EventRouter eventRouter; + + private final Monitor monitor; + + private final ObjectMapper objectMapper; + + @Override + public void contractTerminationCompletedOnThisInstance(ContractTerminationEvent contractTerminationEvent) { + sendMessage("contractTerminatedByThisInstance", contractTerminationEvent); + } + + @Override + public void contractTerminatedByCounterparty(ContractTerminationEvent contractTerminationEvent) { + sendMessage("contractTerminatedByCounterparty", contractTerminationEvent); + } + + private void sendMessage(String logEvent, ContractTerminationEvent contractTerminationEvent) { + val logEntry = LogEntry.from(logEvent, contractTerminationEvent); + try { + val event = buildLogEvent(contractTerminationEvent.contractAgreementId(), logEntry); + publishEvent(event); + monitor.debug("Published event for " + logEntry); + } catch (JsonProcessingException e) { + val message = "Failed to serialize the event for the LoggingHouse " + logEntry; + throw new LoggingHouseException(message, e); + } + } + + private @NotNull MdsContractTerminationEvent buildLogEvent(String contractAgreementId, LogEntry logEntry) + throws JsonProcessingException { + val message = objectMapper.writeValueAsString(logEntry); + return new MdsContractTerminationEvent( + UuidGenerator.INSTANCE.generate().toString(), + contractAgreementId, + message + ); + } + + private void publishEvent(MdsContractTerminationEvent event) { + @SuppressWarnings("unchecked") + EventEnvelope.Builder builder = EventEnvelope.Builder.newInstance(); + + val eventEnvelope = builder + .at(System.currentTimeMillis()) + .payload(event) + .build(); + + eventRouter.publish(eventEnvelope); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java new file mode 100644 index 000000000..8fd10a763 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import lombok.val; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +public class MdsLoggingHouseBinder implements ServiceExtension { + + @Inject + private EventRouter eventRouter; + + @Inject + private Monitor monitor; + + @Inject + private ContractAgreementTerminationService contractAgreementTerminationService; + + @Override + public void initialize(ServiceExtensionContext context) { + setupLoggingHouseTerminationEventsLogging(); + } + + private void setupLoggingHouseTerminationEventsLogging() { + val objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + contractAgreementTerminationService.getContractTerminationObservable() + .registerListener(new MdsContractTerminationObserver(eventRouter, monitor, objectMapper)); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..6ac768693 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.mdslogginhousebinder.MdsLoggingHouseBinder diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82f6b2cad..05db15bdf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ json = "20220924" jsonAssert = "1.5.1" jsonUnit = "3.2.7" junit = "5.10.0" -loggingHouse = "v1.1.0" +loggingHouse = "v1.2.0-alpha.1" lombok = "1.18.30" mockito = "5.12.0" mockserver = "5.15.0" diff --git a/launchers/common/base-mds/build.gradle.kts b/launchers/common/base-mds/build.gradle.kts index 0faceaa8d..89d1a8bd8 100644 --- a/launchers/common/base-mds/build.gradle.kts +++ b/launchers/common/base-mds/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { implementation(libs.loggingHouse.client) + implementation(project(":extensions:mds-logginghouse-binder")) } group = libs.versions.sovityEdcGroup.get() diff --git a/settings.gradle.kts b/settings.gradle.kts index 78a0903b2..003fd30ef 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ include(":extensions:contract-termination") include(":extensions:database-direct-access") include(":extensions:edc-ui-config") include(":extensions:last-commit-info") +include(":extensions:mds-logginghouse-binder") include(":extensions:policy-always-true") include(":extensions:policy-referring-connector") include(":extensions:policy-time-interval") diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java index 525e5669d..e9347079d 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index bc4d8bb10..81f06a375 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -22,6 +22,9 @@ import de.sovity.edc.client.gen.model.ContractTerminationRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.TransferHistoryEntry; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import de.sovity.edc.extension.contacttermination.ContractTerminationEvent; +import de.sovity.edc.extension.contacttermination.ContractTerminationObserver; import de.sovity.edc.extension.e2e.extension.Consumer; import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; @@ -34,10 +37,13 @@ import org.awaitility.Awaitility; import org.eclipse.edc.connector.contract.spi.ContractId; import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.eclipse.edc.junit.extensions.EdcExtension; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @@ -59,6 +65,8 @@ import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; public class ContractTerminationTest { @@ -70,8 +78,8 @@ public class ContractTerminationTest { void canGetAgreementPageForNonTerminatedContract( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assets = IntStream.range(0, 3).mapToObj((it) -> scenario.createAsset()); val agreements = assets @@ -118,8 +126,8 @@ void canGetAgreementPageForNonTerminatedContract( void canGetAgreementPageForTerminatedContract( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); scenario.negotiateAssetAndAwait(assetId); @@ -151,7 +159,6 @@ void canTerminateFromConsumer( @Consumer EdcClient consumerClient, @Provider EdcClient providerClient ) { - val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -180,8 +187,8 @@ void canTerminateFromConsumer( void limitTheReasonSizeAt100Chars( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -221,8 +228,8 @@ void limitTheReasonSizeAt100Chars( void limitTheDetailSizeAt1000Chars( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -264,8 +271,7 @@ void limitTheDetailSizeAt1000Chars( @TestFactory List theDetailsAreMandatory( E2eScenario scenario, - @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient + @Consumer EdcClient consumerClient ) { val invalidDetails = List.of( "", @@ -305,8 +311,8 @@ List theDetailsAreMandatory( void canTerminateFromProvider( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -337,7 +343,8 @@ void canTerminateFromProvider( @Test void doesntCrashWhenAgreementDoesntExist( - @Consumer EdcClient consumerClient) { + @Consumer EdcClient consumerClient + ) { // act assertThrows( ApiException.class, @@ -353,8 +360,8 @@ void cantTransferDataAfterTerminated( E2eScenario scenario, ClientAndServer mockServer, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = "asset-1"; val mockedAsset = scenario.createAssetWithMockResource(assetId); scenario.createContractDefinition(assetId); @@ -417,8 +424,8 @@ void cantTransferDataAfterTerminated( void canTerminateOnlyOnce( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -439,12 +446,92 @@ void canTerminateOnlyOnce( assertThat(alreadyExists.getLastUpdatedDate()).isEqualTo(firstTermination.getLastUpdatedDate()); } + @SneakyThrows + @Test + void canListenToTerminationEvents( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Consumer EdcExtension consumerExtension, + @Provider EdcClient providerClient, + @Provider EdcExtension providerExtension + ) { + // arrange + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + val detail = "Some detail"; + val reason = "Some reason"; + val contractTerminationRequest = ContractTerminationRequest.builder().detail(detail).reason(reason).build(); + val contractAgreementId = negotiation.getContractAgreementId(); + + val consumerService = consumerExtension.getContext().getService(ContractAgreementTerminationService.class); + val providerService = providerExtension.getContext().getService(ContractAgreementTerminationService.class); + + val consumerObserver = Mockito.spy(new ContractTerminationObserver() { + }); + val providerObserver = Mockito.spy(new ContractTerminationObserver() { + }); + + consumerService.getContractTerminationObservable().registerListener(consumerObserver); + providerService.getContractTerminationObservable().registerListener(providerObserver); + + // act + + consumerClient.uiApi().terminateContractAgreement(contractAgreementId, contractTerminationRequest); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + Thread.sleep(2000); + + // assert + + assertThat(consumerService.getContractTerminationObservable().getListeners()).hasSize(1); + assertThat(providerService.getContractTerminationObservable().getListeners()).hasSize(1); + + ArgumentCaptor argument = ArgumentCaptor.forClass(ContractTerminationEvent.class); + + verify(consumerObserver).contractTerminationStartedFromThisInstance(argument.capture()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(consumerObserver).contractTerminationCompletedOnThisInstance(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(consumerObserver).contractTerminationOnCounterpartyStarted(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(providerObserver).contractTerminatedByCounterpartyStarted(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(providerObserver).contractTerminatedByCounterparty(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + // act + + consumerService.getContractTerminationObservable().unregisterListener(consumerObserver); + providerService.getContractTerminationObservable().unregisterListener(providerObserver); + + // assert + + assertThat(consumerService.getContractTerminationObservable().getListeners()).hasSize(0); + assertThat(providerService.getContractTerminationObservable().getListeners()).hasSize(0); + } + + private static void assertTerminationEvent(ArgumentCaptor argument, String contractAgreementId, + ContractTerminationRequest contractTerminationRequest) { + assertThat(argument.getValue().contractAgreementId()).isEqualTo(contractAgreementId); + assertThat(argument.getValue().detail()).isEqualTo(contractTerminationRequest.getDetail()); + assertThat(argument.getValue().reason()).isEqualTo(contractTerminationRequest.getReason()); + assertThat(argument.getValue().timestamp()).isNotNull(); + } + private static void assertTermination( ContractAgreementPage consumerSideAgreements, String detail, String reason, - ContractTerminatedBy terminatedBy) { - + ContractTerminatedBy terminatedBy + ) { val contractAgreements = consumerSideAgreements.getContractAgreements(); assertThat(contractAgreements).hasSize(1); assertThat(contractAgreements.get(0).getTerminationStatus()).isEqualTo(TERMINATED); From 76551e8c2344b7df5b15f9327cb980e3c69a79da Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 4 Sep 2024 13:00:42 +0200 Subject: [PATCH 288/295] chore: Release prep 10.3.0 (#1038) --- .env | 6 +++--- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 816b50f87..c8ee6ed0e 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:10.2.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.2.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.2 +EDC_IMAGE=ghcr.io/sovity/edc-dev:10.3.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.3.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.3 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff6d81d1..b25642521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,6 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes -- Add Contract Termination Observer -- MDS only - - Log contract termination events in the logging house - #### Patch Changes ### Deployment Migration Notes @@ -31,6 +27,43 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:{{ VERSION }}` - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` + +## [10.3.0] - 2024-09-04 + +### Overview + +Minor updates for contracts termination + +### Detailed Changes + +#### Minor Changes + +- MDS only + - Log contract termination events in the LoggingHouse + +#### Patch Changes + +- EDC CE + - API request examples updates + +- EDC UI + - Check the contract limits before negotiating a new one. + - Changed the title of Contract Definitions to Data Offers. + - Enhanced EDC UI terminologies for the Create Data Offer tab. + - Date and time display fixes, unified date format. + +### Deployment Migration Notes + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:10.3.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.3.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.3.0` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.3.0` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.3.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.3` + ## [10.2.0] - 2024-08-20 ### Overview From dbe965e4b5f4a27582092c7e431a57a3d3fc89fd Mon Sep 17 00:00:00 2001 From: Tim Berthold <75306992+tmberthold@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:00:02 +0200 Subject: [PATCH 289/295] chore(workflows): remove unused workflow (#1039) --- .github/workflows/mds_issues_referencing.yml | 47 -------------------- 1 file changed, 47 deletions(-) delete mode 100644 .github/workflows/mds_issues_referencing.yml diff --git a/.github/workflows/mds_issues_referencing.yml b/.github/workflows/mds_issues_referencing.yml deleted file mode 100644 index d31a11f9a..000000000 --- a/.github/workflows/mds_issues_referencing.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Reference MDS issues ind MDS Org-Repo - -on: - issues: - types: [opened] - -jobs: - mds-issues-referencing: - if: contains(github.event.issue.labels.*.name, 'kind/bug') - && ( - contains('AdamRaven', github.event.issue.user.login) || - contains('alexanderaukam', github.event.issue.user.login) || - contains('cristianivanescutsystems', github.event.issue.user.login) || - contains('DanielHeiderMDS', github.event.issue.user.login) || - contains('dhommen', github.event.issue.user.login) || - contains('DianaMDS', github.event.issue.user.login) || - contains('drmVR', github.event.issue.user.login) || - contains('ip312', github.event.issue.user.login) || - contains('juliusmeyer', github.event.issue.user.login) || - contains('MaichiNguyenMDS', github.event.issue.user.login) || - contains('maxschmidMDS', github.event.issue.user.login) || - contains('MoritzStober-acatech', github.event.issue.user.login) || - contains('nb-mds', github.event.issue.user.login) || - contains('robinidento', github.event.issue.user.login) || - contains('schemetzko', github.event.issue.user.login) || - contains('schoenenberg', github.event.issue.user.login) || - contains('sebplorenz', github.event.issue.user.login) || - contains('StraeterRainer', github.event.issue.user.login) - ) - runs-on: ubuntu-latest - - steps: - - name: Create issue in MDS Org-Repo - env: - MDS_ISSUE_CREATOR: ${{ secrets.MDS_ISSUE_CREATOR }} #PAT of the account who has permission in the MDS repo and will also be the creator of the issues as secret - MDS_ORG_NAME: ${{ secrets.MDS_ORG_NAME }} #The MDS GitHub Org name as secret - MDS_REPO_NAME: ${{ secrets.MDS_REPO_NAME }} #The MDS target repo name as secret - run: | - ISSUE_TITLE="${{ github.event.issue.title }}" - ISSUE_URL="${{ github.event.issue.html_url }}" - MDS_BODY="Automatically created issue as referece for: [${ISSUE_URL}](${ISSUE_URL})" - JSON_PAYLOAD=$(jq -n --arg title "$ISSUE_TITLE" --arg body "$MDS_BODY" '{title: $title, body: $body}') - curl -X POST \ - -H "Authorization: token $MDS_ISSUE_CREATOR" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/${MDS_ORG_NAME}/${MDS_REPO_NAME}/issues \ - -d "$JSON_PAYLOAD" From 2e4c9cb64988754b8eda7011da32468252fb405a Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 12 Sep 2024 09:40:04 +0200 Subject: [PATCH 290/295] feat: add CreateDataOffer API endpoint (#1035) --- CHANGELOG.md | 3 + docs/api/postman_collection.json | 38 ++- docs/api/sovity-edc-api-wrapper.yaml | 138 +++++---- .../edc/ext/wrapper/api/ui/UiResource.java | 8 + .../ui/model/DataOfferCreationRequest.java | 43 +++ .../ui/model/PolicyDefinitionChoiceEnum.java | 21 ++ .../ui/model/PolicyDefinitionCreateDto.java | 1 - .../WrapperExtensionContextBuilder.java | 6 +- .../ext/wrapper/api/ui/UiResourceImpl.java | 6 + .../api/ui/pages/asset/AssetApiService.java | 11 + .../data_offer/DataOfferPageApiService.java | 73 ++++- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 266 +++++++++++++++++- 12 files changed, 548 insertions(+), 66 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b25642521..50efff6d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Extend the Wrapper API + - Adds `createDataOffer` endpoint to create an asset, policies and a contract definition in a single call + #### Patch Changes ### Deployment Migration Notes diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 0a7e8e50a..9d8a0b327 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -4,7 +4,7 @@ "name": "sovity EDC Community Edition Copy", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31514741" + "_exporter_id": "32949497" }, "item": [ { @@ -854,6 +854,42 @@ } }, "response": [] + }, + { + "name": "Create Data Offer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"uiAssetCreateRequest\": {\n \"id\": \"create-data-offer-1\",\n \"title\": \"Create Data Offer Example\",\n \"language\": \"https://w3id.org/idsa/code/EN\",\n \"description\": \"Testdescription\",\n \"publisherHomepage\": \"https://www.sovity.de\",\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\n \"version\": \"v1.0\",\n \"keywords\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"mediaType\": \"application/json\",\n \"landingPageUrl\": \"https://www.google.com\",\n \"dataAddressProperties\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\n },\n \"dataSource\": {\n \"type\": \"HTTP_DATA\",\n \"httpData\": {\n \"baseUrl\": \"http://example.com/baseUrl/\"\n }\n }\n },\n \"policy\": \"PUBLISH_RESTRICTED\",\n \"uiPolicyExpression\": {\n \"constraints\": [\n {\n \"left\": \"POLICY_EVALUATION_TIME\",\n \"operator\": \"GEQ\",\n \"right\": {\n \"type\": \"STRING\",\n \"value\": \"2024-03-31T22:00:00.000Z\"\n }\n },\n {\n \"left\": \"POLICY_EVALUATION_TIME\",\n \"operator\": \"LT\",\n \"right\": {\n \"type\": \"STRING\",\n \"value\": \"2024-04-30T22:00:00.000Z\"\n }\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/create-asset/", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "create-asset", + "" + ] + } + }, + "response": [] } ] } diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index ef2167e31..8cda83f2b 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -117,6 +117,26 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/create-data-offer: + post: + tags: + - UI + description: "Create a new asset, contract definition and optional policies.\ + \ Uses the same id for the asset, the contract policy, the access policy and\ + \ the contract definition" + operationId: createDataOffer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DataOfferCreationRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' /wrapper/ui/pages/policy-page/policy-definitions: post: tags: @@ -918,6 +938,24 @@ components: - EQ - IN - LIKE + DataOfferCreationRequest: + required: + - policy + - uiAssetCreateRequest + type: object + properties: + uiAssetCreateRequest: + $ref: '#/components/schemas/UiAssetCreateRequest' + policy: + type: string + description: Which policy to apply to this asset. + enum: + - DONT_PUBLISH + - PUBLISH_UNRESTRICTED + - PUBLISH_RESTRICTED + uiPolicyExpression: + $ref: '#/components/schemas/UiPolicyExpression' + description: Request to create a data offer OperatorDto: type: string description: Type-Safe ODRL Policy Operator as supported by the sovity product @@ -935,19 +973,6 @@ components: - IS_ALL_OF - IS_ANY_OF - IS_NONE_OF - PolicyDefinitionCreateRequest: - required: - - policy - - policyDefinitionId - type: object - properties: - policyDefinitionId: - type: string - description: Policy Definition ID - policy: - $ref: '#/components/schemas/UiPolicyCreateRequest' - description: "[Deprecated] Create a Policy Definition. Use PolicyDefinitionCreateDto" - deprecated: true UiPolicyConstraint: required: - left @@ -964,17 +989,36 @@ components: $ref: '#/components/schemas/UiPolicyLiteral' description: "ODRL AtomicConstraint as supported by the sovity product landscape.\ \ For example 'a EQ b', 'c IN [d, e, f]'" - UiPolicyCreateRequest: + UiPolicyExpression: + required: + - type type: object properties: - constraints: + type: + $ref: '#/components/schemas/UiPolicyExpressionType' + expressions: type: array - description: Conjunction of required constraints - deprecated: true + description: "Only for types AND, OR, XONE. List of sub-expressions to be\ + \ evaluated according to the expressionType." items: - $ref: '#/components/schemas/UiPolicyConstraint' - description: "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)" - deprecated: true + $ref: '#/components/schemas/UiPolicyExpression' + constraint: + $ref: '#/components/schemas/UiPolicyConstraint' + description: ODRL constraint as supported by the sovity product landscape + UiPolicyExpressionType: + type: string + description: | + Ui Policy Expression types: + * `CONSTRAINT` - Expression 'a=b' + * `AND` - Conjunction of several expressions. Evaluates to true iff all child expressions are true. + * `OR` - Disjunction of several expressions. Evaluates to true iff at least one child expression is true. + * `XONE` - XONE operation. Evaluates to true iff exactly one child expression is true. + enum: + - EMPTY + - CONSTRAINT + - AND + - OR + - XONE UiPolicyLiteral: required: - type @@ -999,6 +1043,30 @@ components: - STRING - STRING_LIST - JSON + PolicyDefinitionCreateRequest: + required: + - policy + - policyDefinitionId + type: object + properties: + policyDefinitionId: + type: string + description: Policy Definition ID + policy: + $ref: '#/components/schemas/UiPolicyCreateRequest' + description: "[Deprecated] Create a Policy Definition. Use PolicyDefinitionCreateDto" + deprecated: true + UiPolicyCreateRequest: + type: object + properties: + constraints: + type: array + description: Conjunction of required constraints + deprecated: true + items: + $ref: '#/components/schemas/UiPolicyConstraint' + description: "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)" + deprecated: true PolicyDefinitionCreateDto: required: - expression @@ -1011,36 +1079,6 @@ components: expression: $ref: '#/components/schemas/UiPolicyExpression' description: Create a Policy Definition - UiPolicyExpression: - required: - - type - type: object - properties: - type: - $ref: '#/components/schemas/UiPolicyExpressionType' - expressions: - type: array - description: "Only for types AND, OR, XONE. List of sub-expressions to be\ - \ evaluated according to the expressionType." - items: - $ref: '#/components/schemas/UiPolicyExpression' - constraint: - $ref: '#/components/schemas/UiPolicyConstraint' - description: ODRL constraint as supported by the sovity product landscape - UiPolicyExpressionType: - type: string - description: | - Ui Policy Expression types: - * `CONSTRAINT` - Expression 'a=b' - * `AND` - Conjunction of several expressions. Evaluates to true iff all child expressions are true. - * `OR` - Disjunction of several expressions. Evaluates to true iff at least one child expression is true. - * `XONE` - XONE operation. Evaluates to true iff exactly one child expression is true. - enum: - - EMPTY - - CONSTRAINT - - AND - - OR - - XONE UiAssetEditRequest: type: object properties: diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 6268537df..a024ed9dc 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.DataOfferCreationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; @@ -136,6 +137,13 @@ interface UiResource { @Operation(description = "Delete a Contract Definition") IdResponseDto deleteContractDefinition(@PathParam("contractDefinitionId") String contractDefinitionId); + @POST + @Path("pages/create-data-offer/") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new asset, contract definition and optional policies. Uses the same id for the asset, the contract policy, the access policy and the contract definition") + IdResponseDto createDataOffer(DataOfferCreationRequest dataOfferCreationRequest); + @GET @Path("pages/catalog-page/data-offers") @Produces(MediaType.APPLICATION_JSON) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java new file mode 100644 index 000000000..705aff710 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "Request to create a data offer") +public class DataOfferCreationRequest { + + @Schema(description = "The asset to create", requiredMode = REQUIRED) + private UiAssetCreateRequest uiAssetCreateRequest; + + @Schema(description = "Which policy to apply to this asset.", requiredMode = REQUIRED) + private PolicyDefinitionChoiceEnum policy; + + @Schema(description = "Policy Expression.", requiredMode = NOT_REQUIRED) + private UiPolicyExpression uiPolicyExpression; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java new file mode 100644 index 000000000..3f494f240 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +public enum PolicyDefinitionChoiceEnum { + DONT_PUBLISH, + PUBLISH_UNRESTRICTED, + PUBLISH_RESTRICTED +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java index 8bc4d076d..65a73ef58 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java @@ -35,4 +35,3 @@ public class PolicyDefinitionCreateDto { @Schema(description = "Policy Expression", requiredMode = Schema.RequiredMode.REQUIRED) private UiPolicyExpression expression; } - diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 48574717f..2e8a40fbc 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -250,7 +250,11 @@ public static WrapperExtensionContext buildContext( miwConfigBuilder, selfDescriptionService ); - var dataOfferPageApiService = new DataOfferPageApiService(); + var dataOfferPageApiService = new DataOfferPageApiService( + assetApiService, + contractDefinitionApiService, + policyDefinitionApiService + ); var uiResource = new UiResourceImpl( contractAgreementApiService, contractAgreementTransferApiService, diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 53697eeff..4bd6ee056 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.DataOfferCreationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; @@ -135,6 +136,11 @@ public IdResponseDto deleteContractDefinition(String contractDefinitionId) { return contractDefinitionApiService.deleteContractDefinition(contractDefinitionId); } + @Override + public IdResponseDto createDataOffer(DataOfferCreationRequest dataOfferCreationRequest) { + return dslContextFactory.transactionResult(trx -> dataOfferPageApiService.createDataOffer(trx, dataOfferCreationRequest)); + } + @Override public List getCatalogPageDataOffers(String connectorEndpoint) { return catalogApiService.fetchDataOffers(connectorEndpoint); diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java index d2266f393..d5d72bdc0 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java @@ -14,6 +14,8 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.asset; +import de.sovity.edc.ext.db.jooq.Tables; +import de.sovity.edc.ext.db.jooq.tables.EdcAsset; import de.sovity.edc.ext.wrapper.api.ServiceException; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; @@ -27,6 +29,7 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; import java.util.Comparator; import java.util.List; @@ -75,4 +78,12 @@ public IdResponseDto deleteAsset(String assetId) { private List getAllAssets() { return assetService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); } + + public boolean assetExists(DSLContext dsl, String assetId) { + val a = Tables.EDC_ASSET; + return dsl.selectCount() + .from(a) + .where(a.ASSET_ID.eq(assetId)) + .fetchSingleInto(Integer.class) > 0; + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java index 43137206a..4bdfd949a 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java @@ -1,16 +1,38 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.data_offer; import de.sovity.edc.ext.db.jooq.Tables; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.DataOfferCreationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterion; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteral; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteralType; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionOperator; +import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import lombok.RequiredArgsConstructor; import lombok.val; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.jooq.Table; import org.jooq.TableField; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; + @RequiredArgsConstructor public class DataOfferPageApiService { + + private final AssetApiService assetApiService; + private final ContractDefinitionApiService contractDefinitionApiService; + private final PolicyDefinitionApiService policyDefinitionApiService; + @NotNull public IdAvailabilityResponse checkIfPolicyIdAvailable(DSLContext dsl, String id) { val table = Tables.EDC_POLICYDEFINITIONS; @@ -37,9 +59,52 @@ public IdAvailabilityResponse checkIfContractDefinitionIdAvailable(DSLContext ds private boolean isIdAvailable(DSLContext dsl, Table table, TableField idField, String id) { return !dsl.fetchExists( - dsl.select(idField) - .from(table) - .where(idField.eq(id)) - ); + dsl.select(idField) + .from(table) + .where(idField.eq(id)) + ); + } + + public IdResponseDto createDataOffer(DSLContext dsl, DataOfferCreationRequest dataOfferCreationRequest) { + val commonId = dataOfferCreationRequest.getUiAssetCreateRequest().getId(); + + val assetIdExists = checkIfAssetIdAvailable(dsl, commonId).isAvailable(); + if (!assetIdExists) { + throw new InvalidRequestException("Asset with id %s already exists".formatted(commonId)); + } + + val policyIdExists = checkIfPolicyIdAvailable(dsl, commonId).isAvailable(); + if (!policyIdExists) { + throw new InvalidRequestException("Policy with id %s already exists".formatted(commonId)); + } + + val contractDefinitionIdExists = checkIfContractDefinitionIdAvailable(dsl, commonId).isAvailable(); + if (!contractDefinitionIdExists) { + throw new InvalidRequestException("Contract definition with id %s already exists".formatted(commonId)); + } + + assetApiService.createAsset(dataOfferCreationRequest.getUiAssetCreateRequest()); + + val maybeNewPolicy = Optional.ofNullable(dataOfferCreationRequest.getUiPolicyExpression()); + + maybeNewPolicy.ifPresent( + policy -> policyDefinitionApiService.createPolicyDefinitionV2(new PolicyDefinitionCreateDto(commonId, policy))); + + val cd = new ContractDefinitionRequest(); + cd.setAssetSelector(List.of(UiCriterion.builder() + .operandLeft(Asset.PROPERTY_ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(commonId) + .build()) + .build())); + cd.setAccessPolicyId(commonId); + cd.setContractPolicyId(commonId); + cd.setContractDefinitionId(commonId); + + contractDefinitionApiService.createContractDefinition(cd); + + return new IdResponseDto(commonId, OffsetDateTime.now()); } } diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index 19dd2cbc1..0b9875b62 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -15,16 +15,21 @@ package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.ApiException; +import de.sovity.edc.client.gen.model.ContractDefinitionEntry; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataOfferCreationRequest; import de.sovity.edc.client.gen.model.DataSourceAvailability; import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionDto; import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.client.gen.model.UiAsset; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiAssetEditRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; @@ -50,6 +55,7 @@ import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; @@ -58,6 +64,7 @@ import lombok.val; import org.awaitility.Awaitility; import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -76,6 +83,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrows; class UiApiWrapperTest { @@ -650,21 +658,23 @@ void canMakeAnOnDemandDataSourceAvailable( .contactPreferredEmailSubject("Subject") .build()) .build()) - .id("asset") + .id("asset") .title("foo") .build()); // act - providerClient.uiApi().editAsset(assetId, UiAssetEditRequest.builder() - .dataSourceOverrideOrNull(UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .method(UiDataSourceHttpDataMethod.GET) - .baseUrl("http://example.com/baseUrl") + providerClient.uiApi().editAsset( + assetId, + UiAssetEditRequest.builder() + .dataSourceOverrideOrNull(UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .method(UiDataSourceHttpDataMethod.GET) + .baseUrl("http://example.com/baseUrl") + .build()) .build()) - .build()) - .build()); + .build()); val asset = providerClient.uiApi().getAssetPage().getAssets().stream().filter(it -> it.getAssetId().equals(assetId)).findFirst().get(); @@ -676,6 +686,244 @@ void canMakeAnOnDemandDataSourceAvailable( .isEqualTo("\"http://example.com/baseUrl\""); } + @Test + void canCreateDataOfferWithoutAnyNewPolicy( + @Provider EdcClient providerClient + ) { + // arrange + val dataSource = UiDataSource.builder() + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com") + .method(UiDataSourceHttpDataMethod.GET) + .build()) + .type(DataSourceType.HTTP_DATA) + .build(); + + val assetId = "asset"; + val asset = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(assetId) + .title("My asset") + .build(); + + val dataOfferCreateRequest = new DataOfferCreationRequest( + asset, + DataOfferCreationRequest.PolicyEnum.DONT_PUBLISH, + null + ); + + // act + val returnedId = providerClient.uiApi().createDataOffer(dataOfferCreateRequest).getId(); + + // assert + assertThat(returnedId).isEqualTo(assetId); + + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(assetId); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(0); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()) + .extracting(ContractDefinitionEntry::getContractDefinitionId) + .first() + .isEqualTo(assetId); + } + + @Test + void canCreateDataOfferWithNewPolicy( + @Provider EdcClient providerClient + ) { + // arrange + val dataSource = UiDataSource.builder() + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com") + .method(UiDataSourceHttpDataMethod.GET) + .build()) + .type(DataSourceType.HTTP_DATA) + .build(); + + val assetId = "asset"; + val asset = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(assetId) + .title("My asset") + .build(); + + val dataOfferCreateRequest = new DataOfferCreationRequest( + asset, + DataOfferCreationRequest.PolicyEnum.DONT_PUBLISH, + UiPolicyExpression.builder() + .constraint(UiPolicyConstraint.builder() + .left("foo") + .operator(OperatorDto.EQ) + .right(UiPolicyLiteral.builder() + .value("bar") + .build()) + .build()) + .build() + ); + + // act + val returnedId = providerClient.uiApi().createDataOffer(dataOfferCreateRequest).getId(); + + // assert + assertThat(returnedId).isEqualTo(assetId); + + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(assetId); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)) + .hasSize(1) + .extracting(PolicyDefinitionDto::getPolicyDefinitionId) + .first() + .isEqualTo(assetId); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()) + .extracting(ContractDefinitionEntry::getContractDefinitionId) + .first() + .isEqualTo(assetId); + } + + @Test + void dontCreateAnythingIfTheAssetAlreadyExists( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = scenario.createAsset(); + + // act + assertThrows( + ApiException.class, + () -> providerClient.uiApi() + .createDataOffer(DataOfferCreationRequest.builder() + .uiAssetCreateRequest(UiAssetCreateRequest.builder() + .id(assetId) + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("foo@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .build()) + .build())); + + // assert + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + .hasSize(1) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(assetId); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(0); + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()).hasSize(0); + } + + @Test + void dontCreateAnythingIfThePolicyAlreadyExists( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = "assetId"; + scenario.createPolicy(assetId, OffsetDateTime.now(), OffsetDateTime.now()); + + // act + assertThrows( + ApiException.class, + () -> providerClient.uiApi() + .createDataOffer(DataOfferCreationRequest.builder() + .uiAssetCreateRequest(UiAssetCreateRequest.builder() + .id(assetId) + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("foo@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .build()) + .build())); + + // assert + assertThat(providerClient.uiApi().getAssetPage().getAssets()).hasSize(0); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(1) + .extracting(PolicyDefinitionDto::getPolicyDefinitionId) + .first() + .isEqualTo("assetId"); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()).hasSize(0); + } + + @Test + void dontCreateAnythingIfTheContractDefinitionAlreadyExists( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = "assetId"; + val placeholder = scenario.createAsset(); + providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId(assetId) + .accessPolicyId("always-true") + .contractPolicyId("always-true") + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(placeholder) + .build()) + .build())) + .build()); + + // act + assertThrows( + ApiException.class, + () -> providerClient.uiApi() + .createDataOffer(DataOfferCreationRequest.builder() + .uiAssetCreateRequest(UiAssetCreateRequest.builder() + .id(assetId) + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("foo@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .build()) + .build())); + + // assert + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + // the asset used for the placeholder contract definition + .hasSize(1) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(placeholder); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(0); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()) + .hasSize(1) + .filteredOn(it -> it.getContractDefinitionId().equals(assetId)) + .extracting(ContractDefinitionEntry::getContractDefinitionId) + .first() + // the already existing one, before the data offer creation attempt + .isEqualTo(assetId); + } + + private static @NotNull List getAllPoliciesExceptTheAlwaysTruePolicy(EdcClient edcClient) { + return edcClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter(it -> !it.getPolicyDefinitionId().equals( + AlwaysTruePolicyConstants.POLICY_DEFINITION_ID)).toList(); + } + private UiContractNegotiation negotiate( EdcClient consumerClient, ConnectorRemote consumerConnector, From 4ce6073f0dca0ebd9e933825f61c7267078c26ca Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 18 Sep 2024 09:13:07 +0200 Subject: [PATCH 291/295] chore: Prepare release 10.4.0 (#1042) Co-authored-by: Tim Berthold <75306992+tmberthold@users.noreply.github.com> --- CHANGELOG.md | 46 +++++++++++++++++-- docs/api/postman_collection.json | 6 +-- .../edc/ext/wrapper/api/ui/UiResource.java | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50efff6d1..038f1769d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ For documentation on how to update this changelog, please see [changelog_updates.md](docs/dev/changelog_updates.md). + ## [x.x.x] - UNRELEASED ### Overview @@ -13,13 +14,12 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes -- Extend the Wrapper API - - Adds `createDataOffer` endpoint to create an asset, policies and a contract definition in a single call - #### Patch Changes ### Deployment Migration Notes +_No special deployment migration steps required_ + #### Compatible Versions - Connector Backend Docker Images: @@ -31,6 +31,46 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` +## [10.4.0] - 2024-09-18 + +### Overview + +MDS Patch release + +### Detailed Changes + +UI and Wrapper API improvements. + +#### Minor Changes + +- Extend the Wrapper API ([PR 1035](https://github.com/sovity/edc-ce/pull/1035)) + - Adds `createDataOffer` / `pages/create-data-offer` endpoint to create an asset, policies and a contract definition in a single call + +#### Patch Changes + +- Changed wording on the data offer creation page ([#817](https://github.com/sovity/edc-ui/issues/795)) +- Data Offer details now display the contract ID for each contract offer ([#795](https://github.com/sovity/edc-ui/issues/795)) +- Warn the user when using an invalid Policy Id ([#746](https://github.com/sovity/edc-ui/issues/746)) +- Warn the user when using an invalid Data Offer Id ([#745](https://github.com/sovity/edc-ui/issues/745)) +- Fixed time restriction upper bound "local day to datetime" conversion issues + ([#815](https://github.com/sovity/edc-ui/issues/815)) + + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:10.4.0` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.4.0` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.4.0` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.4.0` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.4.0` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.4` + + ## [10.3.0] - 2024-09-04 ### Overview diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 9d8a0b327..b15b1bf9a 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -876,7 +876,7 @@ } }, "url": { - "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/create-asset/", + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/create-data-offer/", "host": [ "{{PROVIDER_EDC_MANAGEMENT_URL}}" ], @@ -884,7 +884,7 @@ "wrapper", "ui", "pages", - "create-asset", + "create-data-offer", "" ] } @@ -2159,4 +2159,4 @@ "type": "default" } ] -} \ No newline at end of file +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index a024ed9dc..71895a4b0 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -138,7 +138,7 @@ interface UiResource { IdResponseDto deleteContractDefinition(@PathParam("contractDefinitionId") String contractDefinitionId); @POST - @Path("pages/create-data-offer/") + @Path("pages/create-data-offer") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Operation(description = "Create a new asset, contract definition and optional policies. Uses the same id for the asset, the contract policy, the access policy and the contract definition") From e06eb27f53eb2c5b265ba5c6bcb56bc7a76281b4 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Wed, 18 Sep 2024 09:34:03 +0200 Subject: [PATCH 292/295] Also add .env changes (#1044) --- .env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env b/.env index c8ee6ed0e..c585203de 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:10.3.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.3.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.3 +EDC_IMAGE=ghcr.io/sovity/edc-dev:10.4.0 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.4.0 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.4 EDC_UI_ACTIVE_PROFILE=sovity-open-source From eea8425e84341133c203bddc6ad66d5a114e4bdd Mon Sep 17 00:00:00 2001 From: Kamil Czaja <46053356+kamilczaja@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:59:26 +0200 Subject: [PATCH 293/295] chore: ap flyway migration sync (#1045) --- CHANGELOG.md | 2 ++ .../migration/V11__Connector_registration_enhancements.sql | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 038f1769d..fd11f1e2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes +- Catalog Crawler: Synced Flyway migrations with the Authority Portal for its v4.2.0 release. + ### Deployment Migration Notes _No special deployment migration steps required_ diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql new file mode 100644 index 000000000..8a4e173b8 --- /dev/null +++ b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql @@ -0,0 +1,5 @@ +alter type connector_type + add value 'CONFIGURING' after 'CAAS'; + +alter table connector + drop column broker_registration_status; From edbfa31b16c1519b5d239c9fff9af2a1888761ee Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 26 Sep 2024 17:27:40 +0200 Subject: [PATCH 294/295] Revert "chore: ap flyway migration sync (#1045)" (#1049) This reverts commit a02b631ea9a04a0e256ded2e4448e7ebf6effbdb. --- CHANGELOG.md | 2 -- .../migration/V11__Connector_registration_enhancements.sql | 5 ----- 2 files changed, 7 deletions(-) delete mode 100644 extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index fd11f1e2e..038f1769d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,6 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Patch Changes -- Catalog Crawler: Synced Flyway migrations with the Authority Portal for its v4.2.0 release. - ### Deployment Migration Notes _No special deployment migration steps required_ diff --git a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql b/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql deleted file mode 100644 index 8a4e173b8..000000000 --- a/extensions/catalog-crawler/catalog-crawler-db/src/main/resources/db-crawler/migration/V11__Connector_registration_enhancements.sql +++ /dev/null @@ -1,5 +0,0 @@ -alter type connector_type - add value 'CONFIGURING' after 'CAAS'; - -alter table connector - drop column broker_registration_status; From f44e2193d38afb18fb02d8bd1426dfab2c5f81d2 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 26 Sep 2024 17:57:13 +0200 Subject: [PATCH 295/295] chore: Release 10.4.1 (#1050) --- .env | 6 +++--- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/.env b/.env index c585203de..38ff87945 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Env variables for docker-compose.yaml -EDC_IMAGE=ghcr.io/sovity/edc-dev:10.4.0 -TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.4.0 -EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.4 +EDC_IMAGE=ghcr.io/sovity/edc-dev:10.4.1 +TEST_BACKEND_IMAGE=ghcr.io/sovity/test-backend:10.4.1 +EDC_UI_IMAGE=ghcr.io/sovity/edc-ui:4.1.5 EDC_UI_ACTIVE_PROFILE=sovity-open-source diff --git a/CHANGELOG.md b/CHANGELOG.md index 038f1769d..f0ad876ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,49 @@ _No special deployment migration steps required_ - Connector UI Docker Image: `ghcr.io/sovity/edc-ui:{{ UI VERSION }}` +## [10.4.1] - 2024-09-26 + +### Overview + +MDS Patch release + +### Detailed Changes + +EDC UI patches only + +#### Patch Changes + +- Fixed the gaps in renaming "Contract Definition" to "Data Offer" + ([#831](https://github.com/sovity/edc-ui/issues/831)) +- Replaced hints with info boxes in On Request data source + ([#820](https://github.com/sovity/edc-ui/issues/820)) +- Fixed cropping of Contract Offer Ids on catalog browser page + ([#795](https://github.com/sovity/edc-ui/issues/795)) +- Used the `createDataOffer` endpoint to create an asset, policies and a contract definition in a single call + ([#841](https://github.com/sovity/edc-ui/issues/841)) +- Fixed config not being applied properly after a version upgrade +- Fixed Date to DateTime conversion issues when using the operators less than `<=` and greater than `>` + ([#846](https://github.com/sovity/edc-ui/issues/846)) +- Added initial support for UI internationalization + ([#680](https://github.com/sovity/edc-ui/issues/680)) +- Implemented Data Offer wizard wording change request by MDS +- ([PR#850](https://github.com/sovity/edc-ui/pull/850)) + +### Deployment Migration Notes + +_No special deployment migration steps required_ + +#### Compatible Versions + +- Connector Backend Docker Images: + - Dev EDC: `ghcr.io/sovity/edc-dev:10.4.1` + - sovity EDC CE: `ghcr.io/sovity/edc-ce:10.4.1` + - MDS EDC CE: `ghcr.io/sovity/edc-ce-mds:10.4.1` + - Dev Catalog Crawler: `ghcr.io/sovity/catalog-crawler-dev:10.4.1` + - Catalog Crawler CE: `ghcr.io/sovity/catalog-crawler-ce:10.4.1` +- Connector UI Docker Image: `ghcr.io/sovity/edc-ui:4.1.5` + + ## [10.4.0] - 2024-09-18 ### Overview

xxP4Ao&)=u}?574@uA7m4fDauIUY+5b3D{B{*wYR3ccT)c-5QDZkf$6<5Z8eG`r}w<2xwm2JEO z%3GDvjtZn8mToJgmD!lu;9EGIF$Zr2)2>Z@D(t7__lSE6j2~606ZD~`K776#^;e8F zdz294A`SNAhJhHF-6z_7>~WkpTO&M^IAe!-ly75)8gqUq)K8+1&tl)|b!K=q!g0k& z1|=tw+Y?i?hj`Hcl<1v#ZAfY`=2$LzGzuLa4~>db3JzD7UzP9+NLe^Fe%sSx1Fc2v z9ZbWOec!Vzkt3KHlb0NAysF}m`s&JL9mAesn=>d)LuO!1p%}$ zOgI<0aU@^keNFI{$VYrX0nGzKhjG$S^3P)EHi;NS>ANfj;+Z$KuNkphv?2v`5hkwu znyz~(<^zxDJPClENB*!mU-%*OVGZ!x9^hGzFYZ(c7Fqo&C9KBn7H-!7=Um_YXLpsY zm@9;c>XnNK-9_#4F;p7PipNE$WMINKEl;kw8WYG`Q!NB)L)HH{tgo?)U<{k@OAy9T zL>f`$Nd$Nd*;tiMA!SVVxzD>7;L3+b-?-mx@1cN=KU7nJ4cG`>VWnSR}Ey@we@*>;-8b zgi@4yGgJffw3L5e|7m+U<3iuXKq4UM9|)k>Mu2s4vXm*q;cd2paRMQ#V0PKyvT0(c z)((!-urzN6uU~Wz*46Th2>4+o2sl8CA#p^z=$@|dk2;3@1K*Puk?T%a`KZ#xm%pXIRDoyY$jm{AjDxYR8dpEeV=+O}y_qo(62ypz z=3o?L#O9oFI7b9u&?`n+2&Nyj_4gsRvQ&|>5<(f#E0KtqTzLX5{moJ{fR_Fi`any6 z$pgOnbVZ-i7~Ev~oSwe=Q~8^*cAN3NCDs1o&U!IckGYdbuzgHRUx9F$(e8V(#c?)c zH{qveyoI`jq^9Z)VKri?=9mIrD177*uZdzq$o8dR?NSD%w_|{8nZBb`k%l5 z_rJvON07rBjuuCf^`z!cLvl<$B0rM}^8_O}bTm0NC zlC=^Cb@3&KEejGD0}VrWl2=F=*3Y7yjzyxlFv`X?+yi!H5pntCU6n`|wc5Y&6?h2f zYBUUte)y2Trm_QoLWy+SG|TMX#szR`lj%&O1ld@699F>6-;!Ypq9TR@x@l%=T3dD| zCe&8SVaJLNG1n6|Zn;TUYMUs#DJhq8c1Gb=j_Q5J6vK)|zk*>V@5nd2a9-?-X$xzX zO1^Nc^|Cgm?i&$?nzJsVs+TP=MNebE6j4z2dhDTY#qQhG9nQ1BC4kUzZf)5dxN-g4 zXyx0|?)tvlqwF)KR30lCvpicCgP%njCPfr48Y!1)kw7&H5+3)a_-_^Euiv7Uh&00OV zNjOVRH__ka#~*+2`{tG4dhAM}^&lP&5P13|N}S`mls(<0mP&{O0Y$5zjH);+Ak z^VZNoU)ox{#ujZ=JPO_G2?9=U`|+zdA8&?3Jb}?3VFBy;!|2Hl7kDcxHGYofAu|!J zh0a6#uhgrMmvD>KKH98@(rUn9sIMeLYFy?sZC?2lS0}Mfq6Q<8hsW9KY26(d!;%QW zb&_JX00UPG4?d3Ikf0(Xe)pmL?y+;xU!bDb<-KIG^gG%j6E(mps8@_dtHX3Of(9R8 ztY#1fuowEUQ|f0h&`GbX2o%?iA0ePux1Z)#PmKHLS??Yp4UB@-8`o>TEuD$``3$`? z4tmf1aFT)sqa=I3K_+S4e546aT&4ETj+sA8Hno$myPfPHnB!wRk(Q9g{C6V-Q-?7J zq`20$pj%T#9+5+Sd=_?cIDp1@@Kc*#sHWWEu4lfZa7D6fHI&r{*|^WuqT$mSt!WOfCs7-hWE2*||?c5c-58lSUfDT$E;2c;GXHgeP~Z#`Q$wdpafiL4o|JTujq}Iq1`< zZ2A0w*&a@~N9zKHO&@S{`9RszLB+kCof2^Rz63A*;}fslUjCB^QzhFe`haO{C&jXy z^xBl`WuDXV)MA5Qf)3XlsK>XHgcie9y*(o)J}tR?9LNcOu{*xGXq%*@T!k_BbKJT0 zE{c|$riUO+3hTqiQi71SCR)v>u8+Rx!0&g*z=_R#G1Db1yS)tO;gqLE(O~(=GfwCo zKjF5B{pZp8|4h|&DG5<*JEJ$n7E=G)(ET%9Z|9_pM=uJJfW1p;Y?9sx*e8l;1DGNo zVV4M44>eLg@%LN<&ejPTX}H`vWM-wOM-WvmyWXrkx)KYR5*dZV?iaq>Iu`^jVq!an zIlk~7kRLB9*Hp;OTVn&r;krX%a*ttbjNB1#Rv%J$kI{eXI-Ff1=ks%dS`mh&r#>9m z5Anky`UV+hcRD9XhoviGn=dete};%e{l4*U&`rzw9#}myiPe?2-ygbAXsg$Sj#v|* zEJXNjT3&J;U)nO7rrQB=J)^snAem_D;truT5=UoN6Aw;%#heH5A*xFBQU1-Ysb8QA z;nHeG0*Z!uRGIRVB7GvAhSH)n-)e(wvQ$(MkABgPjp%V-E<D(t2CPVdi*tp2<(nq~8GkCSi(judFbnxpELxf>_9Q_;tND z-GqEvy~Z?DQF*tt^|@{TgH@l?s^t2)Pr&C1!^MVm{=Q(q+2SWMe(h-#Ve7euiZozg zFcs9z}F6L7~JYBK->_k3wZ|!V-~JYm}Pp z+v{Kc=HPPgpf>;ejp7#ZGTLRE+HV}N%6cRhLu${fFnu~08uBY447A**#4XtK@trTd z3rl9)$x(UU4-CA6HBl5%Dos z+*&%87V-k84DZtVoWy&Ke2FNzqXKwD0%Q!13;y0k+eRQUA9*$87SU7-lf>IuX?Cvd zx2$#g<0p#KfS_)IC~e(Ld80$YW+i%L`c;4keRoa>K3`gZjU!@3v9_gOn`$rsS?~ToR_oqg0&$OzLr5@{_9zXN$ zkNq>bZ7?fJ3nE)qPyu4~M4ViL2Kh79S>Ozcm)~PQ?AyU=k3o?&x zIlWDX+C_kFo}h&1-TVV0gM(V2aVUq9?#C+pO_s@rw|U*NI^_Uoq9BUxEuF!MYVNkP z0p3eWWjZr#WE^Xeoc&!MU1@Yc&FpNx&|*#U?^clK4>2O@@iI|(qF|jZ#k@V6)}d1N z@VMe-ln@8Fd60&-Q%*`4QTGvm?<{MExeyeLuu%t)H=bOfDFm2h8Z-w;7t$eYr3z&%aI7Q|Z!! zWy(4%_tRz&UVJIZ{al4gIyeI2w6|YGn@Y&ZV?q#VW=G7KwQ}^Z9A5!fDn`>A23H=B zI1zs#phe`h6V5l|08vD%HD@AWB!jSP4hjcuXwTjOq~%{BFD;dp{#xRTm+=ASL=Lr8 zDc)z?EO*>ITQ!P&v+w`}#ojOPV(Hfpq8;KUfaB)f!H1;~z>@km)OZg*#!>tjh$C@K z+#T!YO*CSPq&GA05l56WZgoxm?dh_r^&Gtu3S@=~0|*eRl(i2A+zyPi?&rDYlGI%aVzBGA*qJ>n#9yUP@Wgx5 zm+j&FrBA=%xC1G?i@i77USIAr$RYhYk?fFEv)ziC6F{y@S6hn=>!H^1q7WMFXxdTr z6SWuNENn4pCJq*Srjado%?|4G$T-l$N&9{kp~8F!REs1!3x4d+;Q;i zxcvN#(qBr{2_r8>oq{npzFvo96!dH4NL6ZlAd@lN^(yu>tD~XRK0? zP4K?yg8&HOJw2N}o1mOz#HKSA9_YPW*`-cJjcTp11nk}|x*QR)rWFA@W}-jJY=7lVo#vCB0awc*juoww}L2K|`s) zoJ8uN_BGxK6EWd)Ni%}16K;btc}MKdyToIy|!L2VaO7Cv2QX)su$R=i6uC=*wZ4BQiI(B&$9dU^;| ztgt5IUurxJL{Rs*tn;X6OP4QI1Gp3s&$R;KQ$Cqc40gtJEkvWLXeim2=+_#XKP03bQ|Kcv@s9 z2S7TVg{ax+#OcdV$g%U|jG1=vGQ5E6!ZQ`z7E;e0o9C1DY!mrU8|%t? znBDx~>D`aildqY${V`wD()ZX+vW~!xbw0z%B(N0S;@|yYksD`cE;365y9A)B+x%47 zidq1GuQ|GRyD#0o)_Xs`M3wnDB@*$_G)gn4r%}eu5 znVi*Z=SUAUI;=gvOF7C4bfmj-^m26IAUfgX~HTfm80$o;jEJQ zP=6f())Y$UEU_)@fmCQFh`6cEB8vvNX{lG1X&NI_LU765X>gFA4_z-!KvjEFGO(y_?heXxON)MOSD0kcr4W)g z2QIemR#cXw5~Ce(|718d0LR#rsY!*MBPTJurtK@p_kE|uVt3+KN)thCl9^h8*9I;c zsF;+-Ny*jv?hy=cf(D+ z@B_Fbm@x&T>2F2A4}ZBu&kH~_%b3Rvg5EfpX#6mw_7Y38+?I4&OshTA{;$LG54n*2 z6;vsF*xY2Z{fDGtjTHmCR6qD1hw49M#lH-fM?9OXClR7Ox>aYwV+2kP@cy<*SFU>( zB;L$nOIW^}$S)O^ilOzhv}J~zMyZH<5M9D!C86wt4*fJS;EV?@-oovyQVOC4?MiL% z-H@<8ism2Fhim_I!PLVns(tVx(wn2ZYL4((d&uob5AcBsuv)s`->F|bpmHzsjD#cDvg=evh;Sv04LB{xgshfgg=*l8IT_V>pIFFBgEM`vwnTo_*uO)R^B z23AF-^HA|eOqYnpbTuirXv4LN#ib3jP)xiXwmz57^;P z6mzbZ%e9UtU@-6e6e>+zvEQ}QD@t?y?WLiyO!)WO=uQpRq{#T7}P zUPHI8)R}jV<3d@dHu8fLn;gi$$I=aN!wom|TL5N=K(7WJKs@TP|L`A~BL zo-OV3cAV#9{mk`}#B=kOTBJv6=iuvqT?tP&onumibD{Jv_#0+c*uvp6}d(92Q^W$_01gS)H(!?Tl#E)K51 zcyPCRUxxIwKD2td@_ef_^D`GDoaaf2kxIEwUp~qS{q`D7L%<3TK!y%bpqsPjUJ@bz zS-m};__xNDML9Dk0#)Va?DQn86UI9qSMFFnjcp2t_8df{;=yK1uae5Qf*@iF7e*vI zE8jNZW1Qq>k4;{bbGX7B@tUC3GO>!cNg1YJVV*dgP=I%Wj~lf4YICnLQNuQclugCm zLSpVAx)mvt0x>E}-+Vm&c}J{1?Ht!=i3bg*bt z*VTegL#2JQrfDS$(T<3q4;QcjicLkiIIHOpyr)qw2lHX@mlMvO8}*d0Cv^kBj{Sz; zOqI*?Ez|C3#0GoU^l&}52tB|)NR;PmBE0i`1TL+2I&(Ezpb6*BIi=FqBo$oBX}ev( zH?u-w852t)MHm;W!~)Q69Wl{+Xb^%%0|VI_Mi;98B4>s+ah@Ahr``HtAAi4kPu7T| z0@WH%gHMI5t6sF*c~!C}=hp|n{OcI~V`9Fn{TZb%rxKJT|3HGKR8VGIV;r0Ezk4=N zV0jwZ?Zy&y%LIB|GpJNDj z=k>rpvjEM)lrV~AE}OIs@L3-a2lg&@qgyN3JS5H|m4m4%$0Q+2R+s&A%ybW0J;E`A z){9ybbowxeDfIT!+4y~p)WhVD(_0OxNvH2V*=isjls9fI&T0gvD&-0{eQV0;|9yX$ z8U>DQ>Ww@I#$fBoi>~w_iL)OciMp~NFYDL+=u=ubLziP2Hza)%09>(r0-cE^fOOsZvbA4pc zl*TP%0*nbvz}~&_>4!zkIXwoA-Ns9ot$sU;A06Rj4^nS=pm!1>jAE-{$HL}g;KH|n zLvDh;JyRdV^-Cy}mGg8Mpss2xtCDIVI2VLj zfYMildx*7gDt=b~&Fd9nz5yj_NBV{}an?--ER5aKLazW-0TX{J!pUTWVR1UO0Di$e zAS7Te@H75QVXYBVcnEV0t_u9s1LT|De)wqVOm@h!)n1|I(-BZ>bqlQa&xsw65OJA zerCMvC&JbLF}{<}I{d1)-O8ohca(PIVMhLCxq17y`_t8)Fl8Kz*P~H4Dq2|`%Cxwy zEtNV#0?p>W%_=X$s3+V_Z~5Xrf@m`Ojvd#O`76=?Wn*$>!EDZU4egXq*;HBvP;d9bHVc>rDstoh57HX)f^Q9 zET}^Nvc3}up`Gt+A27N${BM8y-#)Z3Xs$~89tXr*h=edD&>hSJ+DzGS5IC-zSaRxl z%*<$~jxtlgi!==F{R7u0hdRmN@Z%7UjyMNl)6?zI3u-7BJF0`eN0oCU0R*bm;IJ~{ zxR8qm8uk;9lt7bOQUh_)xOZI|LW2xWV`noejj2QEW)I;9j6uP=e<)mWG36VrzHKs==;9v@p7ggxg%u%q1%z3Dm+F1#RSKiA)J;G`6N6A*{ z5duZ|sY;HjUyy;QFWVawc(n8-H$_hPX{Bhul;Z=@gn{u0Nvq;_8*(yBuZmydkv_Zy zby~D$;LsfKMbcyY8gTtka<@H2iMl*M^*jo@wE>)k|D>YgEtz)B?)+Ygl?$_4U+gOx zFZ9SXg4|zfQqQX@^+2K`%3LxICbxd?&XRP*PK|Z?ej2+x8_J7*z>4Mw1t@bpS+b*7 z=CFPJj?P3zDwPgPw;bi?L3Ze~rC7jlH`bM+D=gcN8(-&V<*yw79V zi?vvXr%-Tm&-vZPQORyQgsdjmx3TLbQMO_@>ZT1a0WJ+ifTA!rJszjbrs8lOY$eGoxEu^(?vkv-(*&)956%~Vl2FMJ2Z$FJX9V^E#0C6()6rMeYm zde2W|JUO}Dw#BMMAUtoDTE=|hZL}c&kK?yV1s#E(iQK9_7Wp5vQ*sQU04HO|TT0?E zIcV!S!@E;{dX6+nW5_0t!t+J#2M)mE&q#P8e07$)OnY5Kx1Mw8G=E^>t{J(Gg)R5{@^MFwfr@`Eg5pTRiHe z*dngtoh?FIA3^J+X2XL?emizUDvh25n6&&AEKa(#dBaWVFFQhFv~`WRS0ouS?K~um z5EVQ)UY5kxIZae|HDtS2Ud{7 ziqIGRkWUK4aewWNCQ5R``GQMuf1O-JzX1g6_Bq7QaiGpKlJgsA%m zr}89L$GUG=>!*YeeIHgf_605Vm?2OSzCJo1L7kP)X9rzEpC_qCd`zK(|BZkxB3=;-;Ioa?2G@zr- zbMhmpdQJ*$Pa}c4h0cE*9a_-KJ)(5bao0295Wei+NX?6lc<;aTyZ-DNh1604)V162E=+U zW==8rv6rQw_I{z}1U(*$0su(?RFeU2Cl|LCUF7bAm_uyq}vFPlrm zF^-43As=6#|%)LuNL!b=sI zqE6#4?{LGbP53r^jiB1=8{<@Y$;{Vo$hex&sY3-on3R*`1tY~udJUi*=Q|9gO zQmrh;ZF|XOQ{<`vzMeKEGm;#pdANQPT4K2pM(^$O@OZ8+pi)oDKoVyLRVZKeCa}4j zRJJ+ev5lDJ6lqkU9wPzkZhLopM;YIM5ERu)=dVwI8=fQelExy8h7wqB)ScuN%D3$s ze)s^;pD&qT$m`zI9nIDi1_Zd-jdA$%lPNc1mZrf^BeY)JOkG{S?djmqd;FtRUO871 zoaz@H3ca=-caKOy=(z!?#`e&7Y~GZlJY%8fQl1!C2W6;~yeSiRqwhFntrx(<63l%* zDb@kMI%%;=IF1N>LX7uK^-BluJL$L)se2;otXiW& zJ<_S1yTL!5fWM$e(38&i-qC0R`wx$Rme$3nrg@j;=wb~_uW5yhw_JU;i$-F&U_{c^ zhSkm`Eo`E-z>Zq}wQ<^Hz4?MmiNgQ%rfbRcT?Qvh)2}JHmHI}D|1{zMTB~4K+Ma78 zA7Asszg$r?42q+MTmJ8X1Sr?({Edx>=!%L!)oy!u?7_Re&?Vd|-Ym)Mb3UvAeI=^0 z)~u%Vh{z5GOHu8TzRdM^{Bo=tBId36TY6UN^+tdzHSm$$OPP>k^!>E@IzK*6cJoU# zP>jy8v{`C;RLbcXZkXr@!q<%)k!o+)ZDG)Mt08K~=*s+8y`ryF7-PU3;SN-#__>Sv zRI-V?_6~wJfZt1(YEs<_9xv&$H;x4OXHJhcF>o5{blGE+Zmk@hVKbM)5A#4PHtaMr zXHZ?bXu17ZNgotpiGe{PFretG&hjrhWMvplG_>!I5e{+>AH#kldVKJ52^4u3)hFBh za4)$c>qZ{+B|ff^P5ikd_n-%%7LD#>2I9IE--_>h)ikvokK@fDi06qU`Cncvb68v( zggN7YAc+R31OnFJsXAGbcwQ;kRH^xkfm=AifvlrsOAP3j6l<{Q$m(qEm0=Pp=75csmtc5zQ#SIt^i4Bh{L0fj(353?l!;0 zR_C+wIUeUzH>lL`){Cc2ZH&T%9ql5L`Pb;kE`knHX_2~H`U1J92b>Spdy?cd&9}ow zH;qrPBwIa8&)vZf%A%$kSeXXz!(^48iJOiM`ZZ$a@4Pjo-7)s?0CrHEciGBhK?g^t zT(|2X9`lP^ft@vdSP>Hd$w)!KJTukcQA8Y?lYjG3z9S}0=(pokWBmxb>69H6Bg!-{ zW^3c<*i9!jGZowL4oVZOkMMYK$G}X>-&|ueVOx|NHlUiAGvtu#BI-{@qW5`@?5pup z9daBHfKlwXki%J3Tee3PSI+l#ZE2;VVb_uD0rZRygmhJJaYC9RZq#9K>r2yK5`G#c zcbV!Xg#a=7!(j+Mv<@|YS3E{+L{Sph+SsEA@U8Sa$M{%+)JLaP8H?Ol^EH8p?w^5H zOo~87*rl8}VZ@e-rp!baEm7u<$^s3^q7U!HPz!OoCdCxzYHodgygXAvsJ1f%m>cH~Xkx zY<0V8Oai(USL8`49=3|u<#7<8-c<+RgiB>FV{hMe7N${PUbaQ`4F{KtB9fvqqJL$g zB)M!lN|OZHwU|>xOD5emiV_h`xNXQ!wIqmj^S*RO3`K;ICP$;6jm!O2F5OA8n#W$k*QI|{8RsHh_atXQ!QI_l5E@SeG)(;@@Zx6j=@qZGG^lW zikegfI=EqAa9ZX7@iGzoKqz@Hv$f_~P)IT=F#qN{*8!tg5N&g-IOA@OupgiJMxy}ZZgk8k)%qwr^uYrB24Dmt9jGdEH9eB>(-%RhBoQr z>@CK-UeF+@Qn{&Zg)Is*9b!J3_?Fv=;W@e$p&!5S4x_fbf;kcZ-v-m;@aCI-dVXId zXKD*KqByro{8@FrShKHS#%46qnKr;P|3J({Zu*ho<%A#qRB8>DdJe`yCDo^b@|W)A zSS(`n5D`2}ucBN;dCkPGtO8GTgdR|5R#Yl*DX1-Wz~=4v(TKSvB9NvleOf^itdyZU z_=3iQQ9P5H5@Jpc+pz6ns8-X=DY5}g8gW0L#B#u`(Lb(KH)+R16bJX=UdU^caN(m^ zL>^F=AB1G8kKyEWH2;&ccFUnbU0%PE$Yi@eV#=Ab)SoC|B@O!n+kd;m{~pr7z_%=k zwbP?*-?C!PLdbR_eP7r;R#L#v@Dn&ajYrU|0z|~rhf1b+P7G58gs;|dApb6&f9`>(Vf z-{0f9A1oP?`ph*}B80%inUZL-#{9^9m+e-r`y;9uk|O>pDq0+h8zjJHr=I6vf+V6_X-bNbUMh~&Q6%}s@1>13z>Ji z#RCX?#f4kxMZTYZ(H~<>C;JlxR3|;GlefXWcm|{5`W04+EL5~}2nP_s6l8;regzIh z8MU~Vil{BPWNg|11k33#{`f+_XMB8QEL0Z0t=;}=)9pOVvFC<^%a0lx&!r)z&aB*x zbZK44IPelxH{aaCAN7?E{czXY&@sPL8;Fcl6`)yQ^7@PxE}Pb^`LF_}C-@xz_+81` zD;O_cS2bkf0ZD;JN2(!(`{}y!MVUp1zU`D09u;$U#>@=^`Di;O-(cke-gZQ55BWkk z+tkvL`Qw%GskzneT!>;}??XvB=Bq(s_%gSvbZQ`w_^goDUJJZ*7Bf%9$$)+NQAlKR zRhJ1bnzC0zEbsB^F36&TIS+7?u{T32E>0?{KhOyenXw^}v^cB9WiAG8dg-nj!>xXo zQ9psp#}mEX5|v^}$n~6u%=*R6e(>JN&W~(62ZuW7k*KAf)(b0C{1DlRJJ=ndKzP_p zR6_89rv5BdA6|F;OZTG%t^C29Z`Uq653Slwm)@qDq_3^W?veKB1a?vJO=!{d34WH> zaFBF5MTTCklY)#&Gft<=QP^rlm$C=VpGOuz>vK4`c)u-JLF}ODr;d|ACjV~;Yirz~ z#{u3Rjw=uCvy2}=hgC{=lCs2EJVDi4`5yDSe`Gn&5&OeN#sDEkwqA8R^14nD&{t-I zJL!-W=N?ygMNRykPo&=rWI~A^J&~ z8p80ckjk1BsijDB+A&^8c zwvxs>5e(J-_SX6+?PWj@;b0pthd1Q%tyKpW%gl)lBvchRGy2m(w+Cs_3$9FyF(Q*^+ELG4>sHg`~w(j$V-e#Jo5j8oBUZRVORKrKMVNmYiFRQM)wHD zpV4<0*>Y6#ALr@6><)S=$ES;e7X?Iy5Ddf(p{3)Ry@$;C=9&^iZvXI@RAUk<8^fyo z-H>U$;d8c)BtY~qUAf>n#*Wv_+;_d|s6nA%r zQrzCq`@i?T_cOC+-fu7iGYm`5Ux@eFRt3)TF?bYulJF~=Cd`d?|SHnorqVcDqh2CYc?fWnV+HK z4=PtC1_iH$IJ~Yu$;=-LOl$U$eN-^Z;e;!Hhv0hrie>lJ&uwH617~?W<|4ci2iRth zM(Mo=Cc!6(4DRcN^5A(>M#1S(>{VM*|8@b5th6IdHed&h|7x_ApR1ipe_!!D$Fp9A zu8=a0I8KYgMyA+2myjS6*N!eEhof51sfjvY^!vcK&0J;7uY99myJUW6oiu1>wZ0Gu z&L}mcP`P;YGiUxg;ub#2SM(}Q&PVGWkn5_%+mPi7;?HPaMNO#9d`vK^oc+fV{5*l+e=ig*a2}x{?H_8>zNZT>z zmZCE12uLp-To`I0Ak_(EN>i|OnXGmNBeJ?CzF3jz%3N8`n=cz~+h=#T_V7bfch{L5 zg)4_CGK0s{jzxj{ozCglB0J zf?KR&x^iD8us_oKjArn@pqn+Xp9Q`X@#GS!I(w|R_r2iFvYV%|Ptf3elvFU8hZYu}2xAI{CMh5Nl>^jZViGrj>Vg*8R zPiX}cYq^;oGtjOszCn?@zlocVzjhm>K*9q;Ec`36r7%1l{nEtF0;HT13J6mrCTMLV zi}pk!F}U=iLPiFNKTpKZrcRDbpJgQq?o?vVRB1_i1}MpGGACl~3Vy9j*+Xoqe^s>X{mwk$KV&aAc`)*3U7jHNWM-@|^9lAefVDVb!4cHTkDkPv-}k=$;$?fStEISj zhKe8lNfPkbJcSm?kH`TJOKy>mye}zVeho|9j>1Vc7$(Hkeg&0Z-bsJIiPg0eT#s%3 z^yy|Hfp)ep1CA@}-{+lw68+!p1}Wd1)!*&Lm*?%qeV^W?*1spkf5%4R{}T0BfFEv| zA89?=;q0OT!y#9eD0Ozl-&vG0rc~?xg!-lU#>1RK;$Vng_iHzJ2y*Oc#ILHj&_t`{ zKM5jn1%Hjbn-Ok68&_th0@g2NL|fOjXon+|eWd5{b|7dHgvnO~yK{ls)4^6_h7h?` zV39d40*K73G2p{17vGIs&k#a8dVS3J{p9cJO(H+Y_hv}WEGc6KfX%Y}x?hA=rl2$N zIR|zJ7+?^q=0c<*T}iHUk1;2*UN>Sc3QvTsG%e&OECqIPCs7g}W)oLH z$4Q-v;4-HJ{@QkNeRj=Uc6?CHrbR*c$B)RS!CDv#WePmt)*PB0<$P%M$} ze)trf9_Ki<>?O@6CSNGW6#?1X9+e+Eczg)H?a6L#K==?8ZfxorcbuVtAzx(&9ti-X zKbiMo0RqP5*<)}65;hm)IZfp2)HKg%gqtdtf4YSSt?>~Ob%PHm;q4ZR!$3vlFhLlE zbGXuQ@Ixc3pgBqSyJobrW(kY0d_+M>vY_z%=mL9*KB4yQJ_CXM4x7>QWjcw+OdaP8 zQeXzkRbB0bVZSGJ>$frisv&i~KJf!{UyHZmk@7)D`t3GzEKG0ZYdN!w?qcKHeTN9! z|ET`><@s%SJT9T+&t9s@c+|h%+IXN+?W(VV{H;u3q2oKP+Z{ui3?luZt_WO3PfqNn zMZe04PJ`g3i($e)uQOd|;UlMZq>Qz1Hl30YF^&(@#}kJ)t*0Ay=X9W!?ww>(9P)VX zh#{T!tS~pKRXxCLw)yQ2-Gc$Tt-5S)R;5py8?tU)4smCeM@L02E9(3Heb8dMgc?pdNta`0~l6KFtBEpE%>r!(_=fiRLHppD7URCdyZGQGGRv%;HYv~XMCPyFxRkNyp=L2f99XzlV-Xh~=%o$XQ2&R1!JYp-{J z@RNnOwH%e$ef>`bw>MQN63$| z?XlGr?JX6H}eSmhnfB8Ib%3x*Lw7}k^r(ELCQu|%t zt7hi^Lu&ggz>t3Lz0V=P)PNE}c%jKX2&L4x1?;Tajr%`o8Iht)12d?196kqxOFBOxhn}-;bF= zU9}C%JJ{pKai*2=Pv;|a3>J!rMf4j`$`nx7z<>h)fWG7=G;~tx>?vq4*x}X?7J{S! zuv~aRoRbsT^%lg9a7l~TXYtNKn}blP1|{S54}s{532I5-%m2%qHZT;tHq(|bzC=7A0t-A8xhS!RG38%5}(&DpK}C^DT{a>DXqnR+6n(y8~Uy3Lt>1T)I+icze1* zJ!uLx7>*KWfGzoLDRvI%DE);baK%+Rz=p`LMdSSU(qx69eY&Pa$41qrPyMS#PVuJ0 z(8`%)wmh}zg&70|ODg;ssK-Vn?dFq$lYFx3p~&C3X<;o1e@+eewn0XpV8<7f%R{AG zuUZ|FDBPE6o4$}805`qIvUQ+G!X6!ex7xugaRWNAz}Ktv;=zXe_0QlRC!1WJ9GlTe zz{RImo^;Go2eAKMVTzu}nP8dn`8JhIKUBU6iin5q%R{ObOCsSQn8y#f@`m&FG z{ZKLXp{!A)_e!>`{k5RXFXVDfy(iEqu{vKnHGlL|nZ`Nl(zJE+WfGZ%6d*dmtKN9a z0I@~0CLmccME!O<@8L4^FpU85>y719T*H!bfNfTR#^uw%RwFM<)GV5^G0=+!$41L> z3;M>PPj)-@e=<&##J~(tozKIvEAqX!3pe&Vt`dN4K*4{(gL!pb_xF*3s|m?^y(_$} zND+?D7OxIQPm8EPh>+O=zx)uhg{C=rxA8I&*ejY!pNJL%wHC-Lx%nuOKIH(|& zX(vqcVV|?08k+;GB9E+twM$bq=Nz~7Au(4c#<#T(H)(vBZILHiGfzl>%X4)}VGiC2 zn>cHokst)Ri^gFZ2Weq~1%r3ea+fwpHI~AnwO9(1#lyoLx_~SAU&VmkEd4m|q5jUm z^N_&=&@~AxoVkG!6zCvbIDCV^y12*i93F^|1H(c}^l;u$D1+V#5xC<$V1ZI|=$^Us zZp6Y=A48qlMKkKs%b4~D!Cbg*qnzvS`LIM6S}Yr8A1eW|TEh(b!bK7k*<ZuIg>M z$hiF>I;28exF<`-$P` z+XFvpe*GMWx*Kbfx5 zZPkPUuY_TQtevl`?JEBz(33-xclgeW4A|*k6ahY=?>M>J&p=q2L_asNJPeOgKse2# zNLY?$%#y>XU&l|vTa|US`2*JG&pvPq=g(8lM4Rukf|JYnBlGT6RLaDz1Oe6QXQf-K zZ*y-D?cN@$RfT6!_dI>*|7w4T7-!1NrJMh4dz>-_ZW$F^0}sOkqby(T=L9iW1v|t_ z8&N~px1G;xssx!$|Hw3LF8xG}FSQa2(}WXu{n5>z*F@`5$A9@6wSbz{Ni5b`8tf+y zj+Ys;Ot4?Lps_D9aq0P$5`&Vtvl-E~w{%OU?E27<$~RKNf0+QOWQEZ)2c)Q0ne+jk z9MM|x$z)aht+1fc5@|~jEd74AFPG>?1GaF?1vlB?PB$C#O*#E#@76Ot+V?fS#0k*U zm%L3@z30b)6R={>(V8EVS$1L70KX}o5E0a=UM|P3teztBzcQx_R8%JO=`}*&SDlh~ zeVIN@3-vtJq>fg_P#`bj_yZ{FijIFJ$ge$61da_?g1)ce;Pl0#{x6y&2)SSniv43-qZ&OYNKz{j6+Xuc*I$ig4&U<{4S?v&2u2aNVoHd zaD^RM^tuSlI>Lw=Ap*uUzfjfL^-}Ui$6EZJddxxq+^y=<&+@gy&qNH##rD~DNh){ zqPU3(X*HZ%O?4Y5L?ajA&I#}dL6fN3GtIMF2u5Bed-VPd2NBgo366suUJ7Sr2-i+4 z5+t9O|H+3JgaDpU0MFYgXaOuy7?K|K>Fy)V-zWtfG9LWjDv467bjF1DmuJ$>p-Q{j zO8al;)4z$*ap9ZVQTYS!U4*h{t>Ivq7(>?vLb^lfMl$)-C#*g{JlJLT&C9tDR&yn( zSZCt$e9Ku#fky) z9Uah5X=E5uLv+YBpu~HwVFRlUNo^R#-hQhWXx4vAy0U_J)7VN{8DI$c0!vGn-u1{d z;H_7$kxxyu5x%Adk@m013-bDQKRXh}kD{}?wA%oS6mSySFgsShWo8*AMvAxpkPptk zbTuSH%-$1vDPUeb!TQe%+=Tcrj)}G?bS{|@HdIW>9M(H^6$^~mtlZ!jy~E8>*E)Hw z8REeYu7?No#h|QK&_S}coX^P|=67JRfAI?f@I5q=c^ZvBFnannMfW&ki$o zr8ukLnn-lec@xgt4UNflB|?1aY{ShzUAoP4MEdo|wX#`V8#Vg7A)C)mX3})AOh&=~ z>kTo{t-#%XD1(KtBoY*wYgulED7&gA@CCzzTZLf71T!u*1k4CHO3F_CFY0I-Udtr4 zL}QN(rP7xy6)3>!d`uXT89)=cwW)kx-!*d2ha|p1Je|=QCOPaaKhSy-KHhW<5M3rt zEg2!cxBWiy!h8O(CO3aw==1#rNWvZcB60~jdFS3z_qHQkeTkonH<=7oBYhneXqs@u z;X;Rj5Tr@J<#bz=y7y$9FVYuv<#iX8OwJ}TL7EX&cZ<0veHf92b|}n+G`I@We_X{r zVu)ATl&qEWYLyI*3-z*LRh8{a6&lv(9zJ#aCVyFk>RZi2o+jT1Yy65nlaYfJzpT1h zTD=_N;FE|tLKH3+fvGeImR!-f=9r8Q)%hW~bxA9nh;f zT4LEEJZ?O=cdExH7j87&f@+p)`_7U>Qcy0O0wUj+>-C0>amoCp#r}dl9K+9KHI#SO za}25;I%TfEU;va}Z#2;vaRSB?U|YgOmR|geuvOM=KR@_&k1*sU59#`w9kc8bp4LjC zs3X~8cTSw=%eIwDcvXe^LoG;wpH|XE+^tE|#)QOTh}v0!th!O_S+EOcFN}clak6r%bONX1#*ue^9+CRs5>%pxq09Ns$*5ed( zx8wa1;1p@rlt`y^t0H5)T3{LSPEB77RkQ)pUCmSjBeP94SjMUl7qDV>`ptR%BqZRR za3y6}+Vc@rkT#CIdu1G#rO~^$N@U21yRsKyiSJr=Hkqj>Wg`Q!eb(=pg@+_{m>)SN zkmFQxMp`l9ys@eVKkD_x-cJhbw(}=IQAa+l2LPDJXMJh=evavp6z)N=x#6H%-%syv zFrEol4Q3?sIaGe`d$3C{3!aUOqcb7i2F4xX;B5O9}2^W$bXQ z#nKQ?PU8V}sGUnZNAJ!)r`rY7PL@xwf+YH`%)AxtBMZ{rOWAs9iCJMo16RN5W5~yT z8-AELnUs4n!p(7LG;JpgWkZFooy5`vJSmnjvoZU^cfJG9PMkPmtzZv3RF?Y7(g1?V z@27F`H*a$>7eX1&EpT4D^p!5=yk~du>eNY0_#X-JKUnnN(=wY6c1qUq-J4qeH#znn zgp{3hL~E>br;O!`S9kW1a(+e-rhE1E07M?7<6Vs`#~ye zN$X5`0u4F8mYrXq6(ErUexQtHMvMnkDKa63y^!Zs=L{3VWz)pifYKg0lZB&5e0rfS zg^&=SjM(ZicTZUoaG9>xFF>cXm{$4uRP5EKYG%_zp1(%JdPy^O%t`i-O)WJDfv7Csl<=ny`Qb=U#I9xpy7GKlGTGLspf<;wlab5wdiG$%yD%T z__keYWS%3ZT-j$0CvcDw-X;F7+HeM7PJ0&ic#dA1{d+z^UjswVJTW!&^E-qKxnZJY z;rn|2*8*FXx$nBPMYHz`q>XHKTWoYiB@yUDMD@r@28BrfTVsP6KTQ2vwR{o4=ACEfb#g3|8DLMj*89Qskoa1I%q#_DsUgWGW7E6$N@F0}K6$aM;1Y*_QtsZ!tU@0d>HJHmQj5Vj7zQS@cu=kmQ-qIqd zbU%K-zj}@j6ed;Nu8P2$iVU|)OL?u%5v)HIR=6LSEzlR_KkH8`Fo)i6YUAwuMQ~KK zJ|&WD5lg3WpJ0aEr6&zv+`mPkUBCMG%aum8=em#8UNK0>Zg25kZ!sdy)=IV^X_470 zb}|!d&TgN7fiY^oGkzt^;cmJ2}^|=xc2Kisl@*$5)op; zohn6J7UtiX>L%#jw-<=x;%ZFIA(IrQ^CJ(GHTipcrkh$KPfd1Y3eeLtOo;zCP~t9B zD7Q`)MARay^w|efm`Fwb9Bawu>^$)`E|+myf}#uL&lPRP{)J50Dx&O$TYZ?|ffxhq)#?S>^*KuU zU0{#2-;~$5z;Q{PwvL?)%lD%dy7l^oSzXCCqb3C+*QdVjf5t9o3I0j4a|N#%04;&X zpIzz@mf^4U?XxsBgpya@cT}$W#*7A+UdjQ!t4*Lq-o=rt_PBmL^5vZn$8!W~ zr^8xol?5}k;T7;+dP<(YvQI(mR1%XFd)dr>Zt@&TjE}ea>~?=Kp^51>1pgEGIedpi zOyiG`c`8JdUJgjnldMeoJzFwdhoe_UOpljpl21swGevyL4_T=LE2(5H2(6E#){-e@ zoSeP^?D4_qc?nh%GOk*bkWwP;0h{8(T|J0arRsf^(HLlE)9*nEQte7>@d+RL667;~qa-2tsSVk|?Tnu6Ls$jutKci%#T&n+O4i^;5;6zBibAKF&3g@$L%XD&}L zG#{Cf>hl>GPz$Ln;ykqfp2{aVHJton!6KX=&#|B#wwdTvx$my{$u;tOz*x z4l_P9%!g;OKpsDF-^lZkrnwrxgOPc3Q@S{t^s(T->>lXacJuLeqhBRNU8DGeGU2*9 zU9AMym-)z`IHg81(ueK~_NiN~2ni34O(Xlt!udba_vh>?*>A*9w0LQg`26r~jk5K+ z6gu93@4qJUwM|!xuDGpCC+H;XyutfjG3jjb8Mc<*@#x*hw=#UP*O8i2FiLnf>_$ z!LfnE2spYJg5?gio?4aqPo9&C%y$u?&wkuwo!u`=y^PSZN;WP32P8gKkj z*m#BO&pzLc?e>X7z|F_hG+6EU!liGBv+4#s64p3+VEf5EKI6uXRl{HRlZ5#jiYi!V ztEP8^+7h-#iCjJbN+^rvt`J{iS4tKgR9x;?x zbtLbe)|h-g(^^f%UJQE`QlvvhbF>Pn%9K7Tm&%P_u8MpK{AEi}dj7YMk@j`d7Qt1s ztCvRo@3r%4Rc}6^qfsd#zS6dti%($t@$KC)`Y~$xj59YaMIYAzi)^&|3FdJU!yqLo zbW0wyGUnL?(3W<74G@Bt_2EaKr##LdyOLi-efPfC1JzjCdm92v*2FjqPR%qWAvNM6 zl%DP=9=HoBT^O0tcW6n%WtucA_WMq;&3@bIJcU@^!cDHzibBYyKG`8~bx(o6TT~X( zqHfbq)3v2(HxiM+k<^JpYSfnS1Bd%8*Ykv6&s5k=uo&b*XhENMGYnJ$Y~SXmi=4pu zn!xVq+VA?SN62tuu2>u$|6^2q{`UV*xx;g^rTsI20<$>?KtTo3hF!<^j{|cVoPm^5 zteFffjCE<7AS=m;p~&~+1l%he4UL?t>N7)?zhRPG3oW| zAOfU9m}_!8selHh`$#N~Jqmv->twE2R|Xi~I+4)kqV)gdLG{gG{^X)&xk()`Axqt5jE%CH- zYbq>(3WC>LN6&?`L610+rD|28A8YpE6v$B+(SFgW{>B`&F@|~uAg-jS0Z!h$M{CVc$ zhdyF}>#aQ#r@r*rATnGzAhayEuYgm=LRNL0F5$lGmuA~7v0MqPJM2ZA)o47{-TIW7 zwQ{Rd*|Znpx%Qe$02?UF(!9y1zY7pVP*^t+(^sh9HNtM$1`Hcbnc7b6x5}m|Hqi4c zX0j)>GL>eJTiH4Ieu_E0sW-;D?MvGhO`AI#Ey@{^$!X@;1Iu5?v0z}G&HafSd_29B zK4_Kua3oDa$FKI2j&7>Caq*7nbE!~Y(D=Uap1_NEvTNdpddIng$grv+d;H}r7k5mn zrZ?0@e%4&c;4o7Uub$Gdt0{-=%JL*zmg1Z)|`}4APEnmj?&9b*&6W;}vf9 z!%ZV-p4RNHMGml#-CA3IH#qHZh-M9}czmGv!ruJIkdw3EhlE6UKe}{o?t?9ZgFmvX z2p?D<3^h8?A2CdGw6x4-Gw|_?*ho5Lfmu|O)RcJOt*98#{S_q#Qm2OS;da`4N z@X$=JpoiF9^r;^PFI>EHeGwN#Fk^jj?_Em@((cRB8XD^^^ugC$cvBF20X0;wv-%sf z%<(B$iJgYl<>n=S`g)GJ9zyv$`Ks--(A6iNZuA6dlN-747naA{crOsof{}Tb7Q-<@ zpdKLSuTTq?6F(fCSHyM?Cp{mQMdCx)%vB|bQW#-XaFl>yLFLkV(gY0-0+Y~jH8M#CJ**` zr}GviGo`8sQTyT6x>ta%Heb@wKwq9=mvt?%PA39S4mv2V{lmq~EA8Lphbo5<37Zmf zxr%lTwXC!Yr;J5g5vrG~l8Sp=xr?efWvi{3?a^8|uWe?mnr`-J+Ys+EXFR9@LTC~v ze?lF|D7#;f-RS^PruRRI`6z7z_-(=oB&H8T^ zAbGx3YNm{~&Rwodw#x7ar-mNupo?|L80YcO#rE;$B~r&7vr~T{Pj^GE$SzYcWjqRz)|Go$#%fV#4nhH`@? z?G||d|AEo}ItZ0xl$G|!ha7uV>)c&3U|+7UTD5mw z{+)=1pI$R5cm}QEmt5LPf)lqf<)h!K*_!f6ca*3pSWr`T8p3mJtO(~@%P#g*oxZeN z765?m5h%Wuv3hX0H`L92RMdKDhXzI8buf6c$$*49V>HN|S5|V)RusQKHsR;s#-Y{Kroa4^L9m>(ul1nc86Wy-x$f&`m3<-rIl>p#x zC2s4(QH7Q7EvE{T>&@{xUwsh8`ufPIe)+aHk7J;OYsomvlp?9}7-p~nRvpKp`BC~! zh9~s1z@$uY@^U&h1sv*5moDHfdL^ChAQ75$JRJSF*VW&+)Jy1WZ@tJuGZM7fGY>MRHZr#rIbrp>W!*!XN8S!6ta3gcjV28vYVB0rR= z$p&%nQ*@Ij>!TjwFe|!<`^){QSE$X%x+t_HK3^!C&U?I~L`fs~ zv(u^1g!>t0mj&I9705B4B#G{JVDG07aXyhG#$0x!;u3=AF(!1tCk)UJgkMj=>WNNY zL2B4q{BD;N0P7IW#}!Qzw;5x;6(Lf@LL|u`T)g$J0|HVDfw+FeY86sH<)!iOt{((o zPBa$_%l8-8OEJv7Nm{lSywYR%Y_!u><(*!leieR3dEmj}R6Ft)QQy3M!>KX2*MekH z(WVkc$wR)7y+9E?Ymx|(?9apkm9jReyh^~(uzmGv*h~oPhQ!KS@3*7UV#vdC9Zn+R z!nWIwtQv|C`+`)?hjoFn)B~-1+dG0MU1EIhvV+m)AxDZJu1$c0IO%%iE)7smD(HP6 zP}_Peu(+(vMCV55$xG`V1h^*g58aGkKdiZEJBszqGplCkxp;eR?op+8$#vn7Ccb=2 zi))Vqr2?9ryz8yZ(c@OK`-yn=bLTyIvcr0$ayb?DEZVM^;UaJD){xg?++9&cu%4}Z zd(-bdf7&aR%EE?_g=T&QRDfmH$29vg3G^_J18WI;nXOOmX{lot(cUYn_~|+q#gW3d zt-^qL#fh4$`j-p4S8-;0nLxSKjT_ijYU|V3^9fp+#Q*wnY%pbvnA!-Vjewe_-}rh3 zh@17qi(nlT|2<-l<@!bXVg>f8y{N&w{_FLgFQL**+ZnomqPk?Vv3edFFHb$az!ML) z57V#u^unl-P;8N_;=Gl9#axZ0yQ-&IXKL~-ePuqrer$#_qF6hrz-1)%C70;!aw$!F zhblR8KCr^HdyT&2>B_{x#b z!1R?EKWyXr_p}~S{1t#u)`Vc;yPZPhzHW{0hqZ=5WyGu*F8X%lvY8xQo4u-jdi;s` zby>0c%^jzf#<4p9hceZ;`%G*r9gqYQj@WR{epYtqKYas#c`_J&v^1%yHKO=$Ea~6N zEF5et_gxzWXCawgPcCdn+{v4%b{u^Uj;HAhHH4chO8+1{2iFgu1LBZdGo(rJ@U=5( zAiZ$fP6T;H?6O?uxy4Dp-_v{L+2>4LpSl6v5x+2%nAMC*K!#4JX_x$gvM^IPfcbYs z_lTT<8p`dunyOpI;v>MFZ~I+bJXNOs@l#6E$P!thye{4j zFd$&iaI!p;sgzmIzVy8o+~?0PVk-HV$Cz8ni%thnXxo@=GOEEW6#D(QXCYzKCCKej zVSJJqi+5f#1H-@$-G{268rZ|LH6i0&dvo1`2l%!vD*|DVM|?gj^fRCmlb|B0BB94F zm$Ntjy@+^b7jsJ*ayz@~`>nn7KO|SMXG0Si2y-*#t`AkDtKVCGV1&IxL(B$ zRf(uQ*SD1%=z|){HqD_gV(ap8&ehHbw;5dUH6>sXmNo{fF@)C%e~Iy>xr~l)5t?=h zyIJg%__04_9R3xek&7MVo|wuRfG{>+63JZQTA<~Bu(^#K`Z1vl>(2pfhNfmtG`YZ= z^y4=^?)z>>o-t{c9W>lCd?Yp5KtAs5WZr!@IfP8?J{IQg7ol5w;WEn!W$ zmBO=Ft6S7FCCABMJi5mJEgM|9XKDBk$qebCwj<5jx$yi4|RE1+zko!@Q*==Njh4vVa!+HCgFK6{~ zCCIq>Qon7(IrAIdwi z{=x*smVoqt1aztnyv8ybG#!u90!OWU+i2mtGi~_&D;E-D8I<2VIZr6mz|i1px-fO+ zA%?*nLU?vH;gy09eMwJvZ#4((BJU?k-t@$x)_ad2*RXyuZ^#`nIPX$~(o;qVS!IWG zta!wleBrgqKE&%H)9Evvn${Y2DP@oJcJpRe!p|7bJs6UJUvIOmpN&1uxPQsm?K8hA zQk5*v+D_t9e$)1rUuh`Wt&f+v=XcjP#p2>)-*g^JTRq**%=J4@O`!YSiVR@EIfqga zCMRSee0K|Y_6|CHuHgrUHIF%Q#lLo(V9Do<&6fwP2dzbr4ZLgSBYb(j_V^wR{&$SH zy#Qw(-~JiUItQtnF9m>>J`mZpVHeJiMsu_-z(l`e<(4$7lWkX%!7Qo2GgnLuh1@{Z zo(kHUolTOJ0_%4jU1i;J7r7HD+S_g^dfn@b*zYT(q_ri=uq-uPek5=?1yEDfq8#B) zI6^(Ug?3e@)hoj14BaQcUYpDt9G()k-cbNZtdWOZWv<7|_FLXZd{DWO1rlxU@GL|N zuKX(G9ZIyF5EkmO{&|<4P}~Rt;3bqiCB~W#cXJ{RrG}yk@ms-Hj5(XAIkb+rs|A!< z>*`$xWu>&MsxGO9^!Dgn@4>(^0l=m>UbPIuYaIkrZ?jx|fxU+R^ki4%r6|Wm z$6NLcWRd@u5&OR^iP>xrYr5c_BiJZksvT#YX}8l{8B}Mgy;Lrag&Z1L;{WRxx`|mc zCfd|dio}ml->hv3EW}n}Pp;PVYrN(%-6m(-JVe?Wx5B-{mp(%>j>Vr7ss)oAUcork3DyF)xl#1Pn z;0i#&kIKhv2M%fo?lv@t1pc(zmqN*JMtD+wkv858zFDnG1)TIr0jUXDnn?@HcPpAHSr$N7uYVhG=@{Qv_^TXVXv23e3-D+)rGZE#16*HSJ|2?t(0`jcC|!l0EVrT^#`5KJj35; z-@zy7i?BE=zkx1mB<{=zGVmn>kgZG)*Ti{E z$RQyHdR|?RrBJUaCVNSd!d?VDhY?_A1vmS@{wu5Dy)X*^6HNkE{^q8zt&W6dIVa=UeMdP>lyoQvC`tI>10@ zGuoQvTxjyPfm})2u z{7{{_)0uZxa5FmCjBCt!!U~#1f6{sRa+=}tA{0?Vr&H@epD|rSNho>RBvqk|tGwdt z8-{%^Cu~9;B7Bf8)_Y|FKBct?(`FH*Z)VkQQ$67ypz|(|Qc`e1PAnOfoa95nCt&98XO=biyg+QTQwtkt&BOxx09LF zr{;tUHQ^1mivP@?I1_vl(r@9{pY-hF|G5uFxlwdrP-M|4I!>J-e+y;{vd>E8M7mzI z(Zp%m)w00u;IqD$A)>%SmH*8rRN^!;;tBbG`ZeaRvJO*`yE5 zyQ%XXsxa=F>wHwQxeR!5UhG6zsj^y0gR>M4=B)J>M6hG*N=|1iv(>*fXv{G4zc;63 z!iwn2T4kDgCExk@C|*)E)wo?IpKK_vQ|K3j35lA|O zKed{BJ6&YxplwIUeD_q)Eh&4w?Wd)G^T)u1$$!PC=kUsyIFsP0IL$UT^QI=NQJqvg z-x!i4ls`no=fOodvw{{a;6B47UI0FC*pwA<1B8X6&eF@yaqfEhcUOu{y^2#v0HWbtYzsJ%{!{Rx0q$ zGJe=AUQSg_wTPHvqCmIAz^xpb*7hMQ`v)93c`#{re2PKvr(5Ev>`z3|^I_Dl@5i%o+~lT&l> zbVI}IjL|R7r(2YK5K(rZ!DngWB2OaOYjgw=XLqS~-kjB{;{5`Mo9uY*L)iEl)@C1W zVaDTF73zr#l*>C*JsbUsURlK9d^*X0wYfzDa_yX~rpD&gns#y7mW&X7%NeN_i*w2V zwW|y}O)h&qMx-$5eEubXdUqvQcoz*1syheOKgj0m%t$q^cKoDUR%HuYGt#J0T77JP z*ZUE@H#WzlihBE|ts0rpr%bxJjJ~F=iJ+7fn%_1oIzEbUP^ZKB$tM!TjM`EqMQ_*s4GfbL~&_XHKvLsg+lE;=Q+qDH+Bx*C${94 zyf?-@tozoanBnL#bXPlfq1XRGa!#0E8yCeFEZu&2N3RyK{w+t=$9`#F5S;E2?qfY= zzXvBjdW(9xc+9^KdDSetwEpn4-?17{Hm#~iV|I8;zV^rU2Sf~xfX(32j?Gy-^8@_&70a>44(H1ciUlw>%M+c}C}L}?Nr@uTqkvBQrc6C4&vvmySv zaDsJ)3LHG62XSKp>XW-0GVjp_sB1tpJ0mI?*Wp@*uRm+g(Dw+3cQ%y$MA+E%_v@w- z_VCoC!%qt|=`YQ)%5*P#niJO8=&jpn8su`Q)p+whK~#B{TJ)TCS^H+}s%WS(WyFN` z6BRzZJVed{p@(RGB+k`g<=l6Xh|}jVf91)c&MP0)*=zSfFPT6Ip5T{+HJw2^S_754 zcjjrzRosr)3ID{swGA=}IwP#ctkZ{A`TB;9*MyW`MJRmTAlFgR?#W1t5*si(Ev7th z6IrhLbHM^vS*w&yS-GcbTbW72h1`k!AEE6sxkD1Mc2>)5DP$ z=$)}s>~3Ul9O=5eb7F2}GH)M~p&(PK{Yr+l*0@(XvdfgDVjQ=uBM= z&E{-9Dv+z~w}v*#-k1NJCyh4V!V*AyZM^a1&oumh3ijuEw(JeiBKOtm5a{gtFAl2A zDDpgA92>-hUp?wQ2MjdOa-0r!oK@-Lw?X}R7#qQP@i0BT1mLHPiY~#`EPTfSo4SSS zcD9cuAiG?TDHn>_#F9{nPJq`erAbinXBl*!~4V~133!%*(MY{vKS9M(E*tzca zUjD1ZV9`*!p(0xOI*KA)wI9RZa4XGh4N4hfy+IJWnLmf68f`qc8S#P+4MXoBwvbii zn3s>_=qxWZix_&sCx|x=X&NBHT|I7gi7;v5_8a4d!5ph6xXP7E= z*+tTxht9b5r*M}r17x-re_Tq(?d@Dil?-CIcpH(I!5Ep78_zPw=@p8%TT420>yj4^k+7Os5+McY6*i)CXUmTb?4eznF2gq|aD#FRAy|`n z*EVE?geEfsOGpr8xDGDfsa6t()i8H@W%^D@soLBxe;O(?C4baq#^)aiL4UO^m6jmR6A?xWl#S-BGWsI8S(T8*s zQ^2WsW3!EfIvD}Tt3+4{3IDVl8A9g2`eK*^D-`iV_(<(wjkDM3@#x(l_7sOsoI;?R z*JI+1l6~@33!uY%qU)4&dku~PSojWJXa`UuSc@! zs6@Gh>96c->VXo6>Af}Tk|%$Vu!sW+m$>sxsZ(}EuvF0t|LH}2BHA4_}j z<&~dxv}ZkJN4s$tg=KUjF( zIC0>H-Lu%ybuo|0k1%wV24V4h;@xEne;cx7ZbOWI62~z`ceZK&!L8`}?0TE{w}G9> za&lqXv8J0vUhs8_>sLAJY1H-NOsNzp`$Wf|zTUH05Z{dH8Yv0lQJ?#q;O@3A=6*># z7^cDD+m@|quurH3Xi%9LW|yp;Davx6MrtRFu@8IRZq**lPuBxF*#*LazTn{!zICrJ z;wPQbaLM{o<{hX zb9C6}qjV`oQ|?b1yIS|(SCPPHQhaDv09bIoN z3I z3WWZwJsJg!^LD-T0PuVnsB)MtGu3V?1jGkuiESYabNFuROk-vP81C)h097Ry$OG|m zlOMK7HPp;{L)?JB#N|hiDSAaU>H(*$qHjWWbyH)7GgwQSi+(-$he@o{)^~_zu8gGr z(E7a^FPv~=N61&jG^iI8q1vh>%>T?CtK8w0>XB)R<=T~X4?WiM@RTx8;fMM1eJ4|X zMK3pd4kLNkOcpl>*e6YJvC-aqt8LEc>HAf#7@TXoob}C0Vt^(cN0P2+Dj3^`OeTkw zkzCGN>P(D3NeCAVvQ&bmMv=^3bdvqx-?y%81&{AM0ezOFn%oTIy`1*eqV9MWp3f@mW9u37P zZr-ZxhYC=8QZxvtaEN491|RFoSiOlK+@2}2=PoRFH(4nR+3DhuCx-+(_j&N%pO+yE z-Gna&#Tim7kDRQOgsKXyOohLvP4Bhx)i^K7fUS}9(WIf#y&$EF;~}mSxT;ri2$xn$ z@Qj_^U~&jqXL#e6Jwr*zPCC2He%x{WRuQX2S{b+VEA%;*#qsm#&&S@uGb#7S^u5k_ zGK7J$ZDsQ5vu27obJIEkQa3RO+dIe}8b3qJiQvuMjE60GJI~duVda8Vv*j3kg-$Cj zfyaeBHa@<;@lc`w?`XCyNYkC66`R?o1FCQeYo9-+sWpG&_FY(XKgEviT<2ND^ z{Ae%>GqA6)q&~AT%~V;#WL3&dnn!0-AI6sXJADW)@t+xx_38Kl6)giLsPYq|YPl*gIs)O~#agjshv#7f%>XZbao-={)Gli(XzBeH6 z_XtI!eE$mK$!B>JISrTE1ZAzvDn+dG1D9to?B>-)FIreyW(Srs|-V|h?vvGG@w@AiEyiPQV!7rP6I5>;_GYqI z>!(0fYh=%H@pch1q1E;S!|au9@CBM97nx#c064ZsGHl4Md5qC6CV=Mqk9V=QomDqL z6-6-XW=*c>$NUg{X*+$PBeKSV4VucP?(i<4^J!8=0;OR4fY*(aKAYjYLRl^sR5 zv3NR}@i(@X=l4UMJHi`vt z1B)*TBm!!wjN))0WtMEgqb$OtT5`*+j+k26io?M@=(9D*IU(RP zSWlB^prmBxLR6lhOun9|L3k>nF<1=1h-e|)E$L+S-de4UC&y03ntQZnJT1?cZ(_nV zD!*<=+~|Bnd_Rgp-9aA<|`I-W{s*2EY;ZxuvRJez)$4PyHnX0UHq3`&BPBhV|zn04CDOz@z z*)_Sh^SIhQX}R^3X`&$#)Y#%`I~uJ!jFF{G%OWbcEq`x|-CXITfx-JlG-W~WVAF9Y zgqL)jL=07TjLjpZy{O)`@1g6;J@>UJ(*jH}0?+JSVE(4;?CbzQ3P7QF=-0__B0*5j z%bX4L1+&6m3z=6U-r-Sm)1~mZg&8D0r?+E%)BeB>Xea z4t>%AjRc_p$!2gjQuhJtXU@Fm_#8X74+UgF`9&I4n{oxjU|mXYiBo10Rq@X7+@EDR z$Dx?*?5Fo=ScJ_oQ_?C?vWpNXw32X7sE+2C4NhAG6J;@n3l1m~AHA4jJhz|~O^2Qi zMS%R4K{bUYU-i~vFIJ&JE(12sNa51b7D-F5po&ngJ}!+%trQG|lD;rdAY6QFPAoPw zp~pB9)H`wiAQ+fx4bT3b-bNXhD#+U9J;Yn7)65Uoar!jU+d5_lG`a{m|3hZbc8_~;n8P_klNgbx>SZ5?c z99l}JN07JWaSM1L^>->?fLZuYPz3p+#!{OjLidpRo-OVnxF{E-{t#RJ4>ghJomnp{ z=J{S_+Oo|Hk1X7L3cYTlL9Fv|yTOxkdZ3+;*2sne_n2l@t0?iO$fNr7L5joPZd=qz zlvgS3QNyF30P)W{1jO(*#mSz`o7S$Vcf!3SR9UtqUV%hu;{GDYqs32nQc9Zo?Y@Q{ z(BKEJ{q=^AP&&W%Rt^tbe#6dGSwVTbbgi@j{+|T-M++cr+I4+9zfjZS5ftaWz?nI7 z{hhpu4Vwg!Ojw(5?zk+fBNay%2QfWK#}!_NV-3=9sq#D!2U)0pQi&z_mx=!Wn~)x` zNPv0w91uod@cw`Ang5x2KCX}>PWvlzh#YwpYR5MO{?TCn9T$CFnVm&1n@*QfA)jt- z7d`=clc@`nfthGha0a<|2wWvA_~AoYswlZO6S2NXu+NyF!=D;h5#{*}^BLt$cBSr{ zoM341qsqCB$mHIsg`$!uyz>ycnwy_6`*q8*Onhp?1kf8JqW&v#rz@cZAcKbF)3*h5<@WtBOD24H%;pg<*C!YTxL|3C|y!nQ6U>8tOnYTKkB zNw3<3O$sWad$Ta@Puis;CrqFn=MQF`bx{JjnX1QJ*}&tW&XrK4w>I)uzfoa56G~cK zn9`C_?_T7OSH%O;A-R`W=&|Y^_>y_!&EjAcCW0u?c43o?JaFN5xtAhYH!7+m2d18G z4aN7?vkk>!%@p6q$AGGbk_^60G+DKc z%wFYWB_#gmV>rin7B+dR7MkE)dRqEB$O-w0H(A`E!Tmgy+PqTMBBiot*K8MD=EVx>`r2B25oh1J zAIcwIWMe<``(9QlbdP3DqXbyoEIen3gIYA@TiTVMWlrjSZV{Jh(Xn=KcwvBtk1ud(|o8vJWU&A;UH< zcvgU561biag2yOx;13gkx4Y)dC)8*eOeN>a)qigT3)rUlb2E>U2ue<>JH*9K=7|Fqc2lv5&4 zL@>?~)Hi6Ml3}9xE&9ZE5ljKe%~6Gq|E0FVByIy-x0d-`u3)@Ip;{Zo)WrKqH^ z=H$kzOjgjiq2U(7(J>cF-;pcnFLiMcR6I5U6fyElI~+%#wgJl2{$Y8|AvFRj+6>U3 zNT+GR6^ul8iTL);$oS(Y*6Jh&v!^Fc%@Cw}TXx+>d*Ib>yT0+Afpt^iuSVKJe{@P& zt8^450WoflAGMah2+RceBZYpI(I6Y61o_z!g6!SkM5Bh1*{hdQ9LyDV$}qMZ0uBv& zg*E1`s))MQ74F%HI2aEI$*;)f%N1oS`uhD%Epyj{%O14b#mE+ur-ySiBM`liGAv6= zhYvM`+$n;O%Z0n%KflbnsmEQl%y%wGx8I*v{B=ROXrR>`+YDhtZAIs{nA zNq`t?z!!tw_RaaGrsN+uQW(3%KN-{j-GTF@+a3pk&Gh$-Rh6#9Uj*a~SNOiMV! zHdwma{`~3-e~-Ar3n*A>rvBp)2?cc_O$>yU>rhjyhdcP2B0U+d^tBU{z|K2;tezW$ zHty-#GFY%a1?_9|qcM95?50QuQ+_sWc_m3jwr8>pOwijTQFmyw8w*K?B45bi7Zn=nDKB{wn)WdPbO^9^z z&uPjp6pbPreJ?bLxK!d(RetJoYR}e=MrV6f>;XFp2*3^e;=(_U|08?`mvi2{;5L6C zbxfL_BiiA?rgzue`L5#BiQOKd-hA>7ezuX5p>-Y{#E->&5pRnwLI>A<9@~?KbR27z zm$CZoMr3E9tx<@~{7>37&6`3iY72#>0V_6_(1 zzL>G1%A7_=!iim$jQLR)f2od@5lsbp3ZXl``}}r5Si2zT)-#^Zmlo5zcq9W#KRP6e zT)juXG`Tnwq`jZL`t9DQdWhmFz#Lu@JByS#Y=D~0|Jf6RajF0yEVF0J8!5ecW2S3( zRv>%oABgj1bVE1g&iK!LXRH|_jNK{-7T{3#~y$xMBbUYm8|2}kEE~)9Lfv*DgXO8 zZnzFsKR)}35J%K?bWQLW=7@5 zBX^cbW&}s_-hEDHHhn9V!UEsa)U>Mme22B@cHC-nSUb~o?0MZxQ!Q29depS9*Y~=d zM{WT)D~Z&B^O~)xprqmI7?6NVd#pw+M$#Y0?;kdAQoFoHpglg8nO0c#&nb8g8jW_Z zykc9RfG&TzyNRtGL#%Y6d`Dq73)|xZ1Mh*m`Xgm%d%2|-L3G=3Nek*#;PELqBxHk( z3$$vPVug?2tCPcPwfyuJIsMo1SDoTTF01kQcC52Tp6KYXG^VypseN_6K;*pEHlsnV zrY7_&OD-l6)JP1tG<-uYE!Nj6&E|8Vkq&SB{Xu#ds4~WO^0vFNQPJDF=FCsa@($-+ ziGdueWH$p4qNr6SNK0ZuG>_CFr_9X0p^5x2wnH>b(^JEbpo+VFk!aQCCMVMmUuieW zEeRoF_3)3VD8*qiM`BPHgZ4r%h&i##5WDukLO;oGS-+KQ3A{ePGI#Tf=I3-R0#|h8 z52Ty9+e1r#+6sGvvUNsl592_)p+5SFOKpW=2u?RXB{^=?wcetA3(tIg0={)BjC%@=6YB{ zyBqa#dy7?)d6SEo)`FCl1N+JWN?XB0jSNmkvGmcsq!yRqrPJY}vA1)|SV2}6dk*Eq zJ{Bl!T&sVb1DcOnm6+a31}s zPLf(jdMya3WO@@{uC#vcG8v@SGq0DdR1wh}SpBSt=fh44e1?z&2!iRG4?b`lx6FTR z0&35umqo|9{w~R5UtWy+6TuR0e{WTK-7oq8-YeBF%KsNoiiZJpjsNjp5Q>A#J_R%) z4^klM#6)J8-7q(mHYP`)z=|DbbpeD$5**UsX(gMu*{uF*7lZHlIW4F5Cv3N**hA!J z>YN~CGFNj7*w242A!fdmQ6)tKQO-??Ol=rcy*$W;v#s-D@P_X*OZKQh#w3eTXtz+k zh$`^zSfIty-7S6bEp5Jh8C$l8TLFiult-JbUv_EcaAeajU^1HY`HC;@kYomI>xNWZ zf&yth|L_SrxA&AaN>&mYPXhX{oo{J>F3slq(o2D=zefTR!cti?qP`e z#9zaP#=-RZec{O;G@p*yqLf7b#df|I`BY=mDPNvq@xEI278L`xczMplcGS|CO`fN7s2pG2YxX)~9YLvTYr zw`X5$E<*FW!FuV+exn}AFIH4_RH|nwi@~kjS1WbzC0oX7RpK0iKUsGOm9e4+FTaKm ze6Pon+vD-M`}*FYkfUveoU{7FselH>ejHb7Eg*pn!>98zN);Y_J8PNYfl;eseFIsU zeyu2#6239w>M_GPq6EXOG6sKr@K<02etT80;Lqi{QaMT)UH6I{)`rwVVWUPACHc&K zw+C_iYH3uO)M{gbFb%))eY?=p^gi-R?O>u7kSW?zkmIn3RPC5RuT@v$p_= z6{9i!Yi?lTKhFd$s$}Y!HsR4kL%re}G+ry`ryl)^`e|ZHx?BL%P!kY@e;^ps{ zP+smO%Xe6!dDn)zScGGu%t_hUP8U*Zp;0_T0jn0dIDqmp`mGz*UcG2y#RE*=uZl~` zeV?A50*ilCw45QhUzX?wJOicJC6V7)%@dzj7AzFI5jRiIRVf5tc9{BZPWmk|Elu;Oss!q+MY2E;%zffW5ZjDY|Nl(XPhNjJ7Tt6*QCKUhl1 zu(7ZffvWj(=dPN6FlJ?lQp6<+wL($Vd#90D&XsbfNtZEFli2#10rin8c|u&HZ__$8 zQJM@p7S=6a`l~$u=S_zNwznZnL4Zt32I{{Aiz`-%z# ziVJKY(%$rC7YO$Wsod}#TF%?0z|stz^0g+(V2{f$3GfgkH9%=~6ps#i zG0flaCGDq%B!+H04-%_A%jX!=VUyKx48N)i$GBNihPqgSv*j6sZ#FJY)(p*`U2E<5 zm~Ba@Kz;}V)Z74&Ji=+*&hmt!tfl(x+#h8jk zsXZ|{k?GpMkn!w`gQcWK01WS*uhI&4O@w{eM9>f`!8fDaWoQB8f>>G$1Rye$def)14k^>=V>>-wSuZ7!HUOJx@BllPmx*Ckdq+ zy%UdwCjYn#dI;hk+d$@r=1Lp8F2g-@bCW*ucDAmNf443Whljnke&hTU4|%zuKxY+w4+8jJndrdS zSY1Hbm@fZw=tt^i?h#j*&0aaW0NO_{qFJ|l^FZR_1~yh&WL8OnwQ$?OQ=#A0jubG- zrGw(t+r>?TKN+f0#TJd$si0b4``6z$eiehQBbn-LT>UuYT9FN+bZO<8t&seoY;HnE zoQ!p_=n_1z*S`;^mo9r|)SEnIeit`u&Ju+S^~KppNJ_>&eQ}w;AzI$ZF8gEnHT*qG zqA%*b&+2I_J?btR_2ML;`^NHiGE)8UqIFnK5t8)PR9Z^rswN;6{bGqY6&H(&xPQnh zH~$z1kZHA&^MCdm{vBjjQ>5XmwhsvR)$26ljC?h| z_h4&9=DP!t`-+6e=+Z;139$s`wx*;wE&{N6$hYS=&UN{w!p24U6wnTM6p+g5nTiS5 zX;n3#wTbx<{XJaptF-W2E0FtNR50(IV=@Mrj3WBkSLOoke$U{r&&Xre7~VthW4fO} z^P&?qS}nwmB@EffLHNJzz2>gc^x9f}waYQ9t03amEU{SYxRSgnL*zpFM+qG9*-bAD zeUecA)@=IVy!U)I_;4)h1sEJHT3zi+_9veRLe%iy#_ZLeb|kd26GN7FAY-b!9vG2) zPtFa05+%x?)XaX6{3=nc2xRa)4FkNKIV4f%+oM6B-g4%eu#$B;qRpq-(YzDQLg`HO z)|vG>nRdFj#{XyI@uW|w*0edTOLz@!fZrZ>_=`;OLa*2?B`0>0$rJ~jOy7+Q+zG`$ z4zlS`%{ugbDm?XZ0LV4i=J-6RapCM5Wm31QC?DO;{-%j~_>&)@)XZPP2cBQSyjD8- z%;%YDNZya`{BZPeRd_5vbxrBz;i!gBRcOxw%l$0BBpuX+^t-#vR)YogJ!6%=?|1HXI%p3 zafHf-*>rE!&HbVC+7h3|dM+J|rZvS!5#;drK{r!@*>F;jY!ee&Fq zzQX*ATdc=LSCkFH%)jQblBoJ_EUHVbRDDE!4WOjf%RM(lSgLgDQ<5cvHk@)s9S?c^=Vl7_5IA@@G_P2*B>|yL-ctK=39WFnwJkz@b|9?w=*Kbvsw}x4-1^m@iO3Y8y5n0mC4M!5{a2buwzWoYSF?{5?oi#)Y{X(}* zyNveG$bE45dn(VyJt-5+2cVceRE|Boc@MFr+iRrW1IMK2F)if&&R-~(T^^XdTDy`@ zx3u^&MIf4w%#@ybsUFu6{`zIDWTCa$FzVRUN1kzyD|c`$zfTiPg1OIsG284F6{u{` zUp@l)fMxATxk3LXG{DRHT2lUwulvu0{tJZR*Ns>IZ&vKT5n61x5X_PEa|ix2j6V$# zkk=y{zz4j=O8ve4>N9i~;0``fW|;p1n6)Y7@bFnbo%}+kQ55kTEr|SFS zcG}ghUTysNVot)vDbnw2`3Qvq07GLL3{FY+Z1+3yN$ax=n}>!m!6lh~+nq-nQs-5; z+28&FyBo8w2g1_08(}~XaV1l*8>Fg*PDvW~zObIL7KLr7RN${r>yEZ@M#9faT1Su3 zf-*-7zM!^mB={GD?rFK7oU2W+S0b2Ks_qnc+l7DQnfPA9`gwZEGfgFtxilPQAlQ1; zv9P2?gF&K^WFAjti`pY1OPS!9 z+CC(2BY5Xa-g|$3e@gJ$iQ2WfA~|@Zs37t)IxY? zHk>@qb2O)P{E2_jG>;Had|tamM<~i=&9w+QNU0FhxI`OY?x}FF+-s{H&kzYK{t%zz zo4Iy4Z;S$euh5v1aF?u62CWk(V20aBrUoKDRwNyH zGtC~{4AnaW{jjbC#f5R*B*To?o9#2jo@1TzgH2OBZGW%hCmHMy{znRp6=M_UC@|c* zMF=Fz6%ku<_6O$hJBs`#uc(^YMf|3W{4h|o|UAz04hFHw+ z9o~p7r}qYQsquTYzr7P$+2sFD@qR)t{Hql&FFjTaE7J@YiZrjM+p(#sDbzJ`$;6I> zxtg`i=gk>uZ!}qM6#DiDyD=7CdRl2z3(YPnt_LjAF||(2r;m&c9jYY`misjPCxhhj z?X?=6yYrZ|$w!(lP95N3MyReYB{30RPL}%u{K?|{oFFY9P@AA$VH0kz8Nah@c6MR# zIgqc$f{#^vjM727BT!+IA}d;@zETjc`+@U81#V^A-C&d>qT*hks6mnW7xcy~=Aey! zI2pdKs@Q>vG&@q5LG$MYLXwjVNDn+|c;WdtPIuHOaO|y0^Z>0=vra7$CiLvQ_?_&b zXa{KgYl32qid(|gS%Lif>(Bif$omFRq+Z+4OmgKgP{ydo50U?60YuQpztDxuI_+2d zIkK~!oaN_;SxpT#gm6KRS+nqPsiqkrGpuur)`^`kPcb&y=wwrPQvOG*ruZ;vgp8&C z2`0IvPFg{Csl|Z?*X30C?d3Per5>8$~~!?nO>Hq!7k z6NmXN&QF>@34uSXOMLyad1k6O*jn&1i9=zT#e8&c&GwlL@UmO8H{6qHPP2yRUO_$i zN6%6uH3dH&V_ts68r{tOT3$aFthZlAXGYk3e`IjsR;X=I=9RYrVx0ECTjHBjBstk4 zJ`w3x-80pAkLiOLqixd(bF$4>aXm^nbp*&FTZbqv#(!mz6O$Jm#QN3VS<2t34y1a; z>fsSGa|-@nvPddE)b+3Qub9NzWK!W6l0kUL7VR$=%|j@;f$b}PK@~fUg)sl3BH&2U zn0plGjAA=#^Eg-ybS5(0UccLXvhm-1+`$llA9=aJ5}wn_wbqd0_`BMi<6%#qKN0?|nc#7d6VFQ1^0?L{gX0 zYCZJoHS5xifklq=tFdh1l;&F#Bn1DHB)E898HF&n2w&r2AsB+!eYSQR&5uS#Qqm#K zN5kdXr%qkjaz&&&*U|Z@%$m2FPDnSKJ%1T)HF*6o>oAn=E7qk=|Ka>Hnc;5PQm1!e znJ#XijvC=wBer=$z;8sx0og#K3=bXMsM6vL{sNUL~AOy0C%ZQ+Gk*iN*#@SuGW zlbU@GSij_sQ`nC!5#Bt~+7}8|@=&wgHh$n`t72;@RjQEwLAseG4dcW95q$9iS5&wg zUwm%9UUt*Z^vG`IZGSi$vYC_@?R>me6>7a+u;y!63&aEdcSLV#fn<)N11Zj^H$MDm zzK1mcbFr*DpqVM+`&`1Uu%_d1nF9Tyw{dyG*}|@Jhys4P8Dcjt*j!DYlXD9sl(&X? zEroR@$DvvlrD}U`+W!>$Gt-O3`#xn1s;kt7E$Qf1Z@yC&=FUDg6S*?dXlp6p*aX6u zCfxIW0k5$afw1@#P^CJB8{AVqtZ7X#(tKEz3Iw{@`N5G67GSujqKdL_IL3$WzWT&|D99&fMn%I9;oHlUnq6JvqG{(ufY&c}NJ1vs>^UOODZ3T^``)&7oxO_( z(QCm=JoYn>xzD`&XVdZsoo`+oLfz@&sLNcj}(usSvJcJ zC&LnDWA%E)+9Soi75Cq58X0HwT<>(f*Jd{_sq3B7eI8q2?!Re@}glBgjD?D}tb%ch<-`1|bXH1AP*xPEk4Lw)} zYl);Qs^HBAmaP_sl9?(%GE-0{;`d*47@nDYEccZv6Qov-GemGa;uhj&m!kTMcc0d% zui!e&Nfg9pj~&@6(>(mVMX0>%?~${uZ+Ksi_=Z%qh>)SUxaYKaRjvIJK1GOLDb`=@ z4_S4!WY)*7&CYu0@u(kCR}d0~P?uEkFhazs*KuTxF~$zf01x?u(t)N?)S_EeKr6@Z z(+p6aL?c@Ldzx`dO1ll1iY(-VyvQ@57-q>@L$4O(DT}U`u4k&Yp}X+c>c!v#SS3C{ z>I<8gX0*+`d=u0_dcpj2T%py{&Oll|?$hq>_?V4uGY#dN?O>ark8{bab`_EMLBqhA zFwMv#Q1%xz`2>|dgBb7TWBidnsvR_jE>iT{@egy&^GgLjXXD+Vu{fiMy$Xg5$>G?t z89MEzvh{@9Il0z)^O5s;p|*lahoc_`8bivHnP0x=n_gQxEnjX3Cy+i3^I`qx@nwDjQJcTb6X5t+yVd5XbpZkVlQPrmRlZH1458N9;cX`<+^wFc5Hb5e zz+;T4^f4`TeRSuXHuTuHdPXv1+}H*jqGFCeSf1Ye8u{Ca&ISKB^{W;PVq&oGdEV6H z{LV*`+jnrL;PE^k63O+NOia2x8Y?FK7}?drAFA+*u3P~^LQo=F)i7Lo9$U;y&z^sD z*Pq_uVrz|keNN-lOj1-*E>Ll8nmKga7~aS!9sHcx)1$$5iVw0PJa(;4PbTuBM?nra&Dq63M-1 zFvE=Hp-s8HEtjk$4gQ7#N>>z7`-jGM6cB$}idU$}1b_#(>&2rF7LN&QIzULUZt|Nm zVLDa{<^g%py?L{5+eU02oB+~Qc8FWSdc<^Rul%s%<^$giQ%_hRT_WZm^&1L9SLJL1 zyn`~;-3s8_z>$#+Y$IQ<=C4@Ah);_F-Bkqo!A=`lTy#_r2D3~Cvgu*u?+9IS@O4O&%)#P9B(#Mby7J`-r`a(D_Gb6 zV42x>8Gwns(d4x#Fl)lefR@)Px}pZW(#qsw{{15DmY)fwM^$Jn$v!)rCJEUNnIsvL zv})QBmeanSVNpTreZ<8F6|XeN_p$9MVtw7+dFbA6yj|?~j-KQBmBpnxH7qY8`??=- zBo+tmrw6ZXoK2z$wo~xRm4Uu@p^2Zz&r_zVR+iVHygD<_C{r@CvP2*xm#kmd1^c|6 z3RE0&|2)xB1`$&af^pq8SbV9sjRm_~+XgtFe}7Xcm0M!1phFF1I74Gp0mC_|ANk1z zY6(Bqp*DjTWH2zA2tft+qoDf987uj&CgxOA$GrSh-O$jW)KL#Us-N(6X;`nvg=J8qACgW5X7q}0`h{PtoY zVf%hVi3)@BdNHlJouek21($f~B~RR5?cw`e{QE?a)rjzlVE#&t!?hs7w?m0j)4V*V zJ!orPhz$PC$8C6#oPqn>N-023U|xNUz0ALXi~llkIdaFd{taBBmx24rM;Z73N&@fb z3wSYRFf-Mr;EM^Z91yU}P-7t|ckPKGueChB!b)2i`2}SFr;0&XHX!puMea=P%6q_)e}&M@AlLE*O2L@v4PryY7&G7IJ{-F-!iECS#H^~rHYLKqPM zl7%^_ilR?0N`b|TJhzJdPS|0t5|8AUJTnDlp@@|^-F`YXjB|S3J7V)EwTac^`D zP)~8_iy?>2N>Ijm?XOl6X%a99kiup&a#$s32k@Y}Js_As`l^w1xb~|mXkGj}6Uid2 zCH8kH%T4oA+q8NIzkYZFsbUXSF%n-nj}O(~G2dzT8z}cnoY@C_zWI>%(-pn+)1MJW z@fHr-bpr1m8dl^ah%R}4)ZH4bTHPl##O+D%Ttab-decdi`& zgO69AO2-sik$+>clHNZ$&TtcHoVAX?e_18Q=b<-LiT{ZD1DD*wpLq)4BwU$aGW9T4 zAp=O@vBQ*%Ke^W_TmPQ1aRaSNF2xE*O$5klJah6qv#oL01?}ufba=Y>1jS^3R5cWh z4SoX`z#H!EuN#fAuya;0Z3D&Iyovsbc!yDt%DYdYjz{)_uQQj-F_7{p#j)RH3`@c3 z`Q}vEqZUS->nsoQK^syD0d{y)qN)#vz#S(QFd4yYvmcirg>5^fH3E=P0rH6kfgiu< zRl}$HCXH*-QVwbUcIo}OCyuMGHu42FZZa7`S+ucJIA@`(>{_YIEWX`knvae9@Rcdf zyFTND6n7M-%yK+P7Z)4vj0vkIv018%{_XM28u6GC&%26IzADbAinL{BT}5rJ^a| z<;90@KPJHFc4aYZl4{u}6art(zU-V+!GC}3^^DgSMTj(`<7OEFb*T1gx>Ow+fbzH3 zw)vhLS0XBguj&A+$Q~oG+6b{1r>Tg(Bs3kY+#d@GcE_JxgfovU(m2D`PZ3xmEgC1y zEHvuL@%wM!&B_qRjs>Ool7UwA6~dqKI_rVIPAICFl>N;s*bXkAj&ryxY||7eMwl4v zzwrX<&#eqrhzK@JOi9L#nF>qe=oAuS+gO&VSMXeF;~y>HnR*}GYOd@DQzS95vX4+zxi-rbUaj?Qwa%qIqpv($*Zm@Nla z2vUNWlV87sDJk?2)b*(THqQDj3|;u$(4(1Be7BE)eiMgdR~t)pbaRtMwgzsu8J^ZS>)hbcZkiDsO^?hGQoZ3@E!UYk@42ohZM696YuO)C+vxzT z&dqK@kzV^GCn9~SLg;(&by__Ki(f|rT+X)yR8^A*`(pqU`lu}?<1Ys2^CkkQezp4S zpXc$9_rQ7KJ(xKpX#eUO|J9)XYSsT1=h;G(hV3ywS@I6+fH;2g%R?ueJ`{${#2bm^ z#d-4CYF~c9TIwTF;mi7pn2sjv1vZDndX=yVZMsWC7iZjMys7e+?JJ?IZpy7_LnV7- zJ3bl&(hHcMnh0t>`bpN8SSIuR;tPf%g~^8s3=20|NK?|Iuz&Yt_+aiton}_#;Ef8# zb}U)mtK1KD=klNyT_DB+bns)sABYgJfdx6sY2W^8bDq`jwoP-i+}FgoL~mCfsI`sP zrrAr_FN%bN=Y^4pRhB+?dbyKf;_pL2IQl7zP_Iy3v0y^u7~;8;-_vA~RWU>sNlKid zg~cBD;nw11vtr~y>GJFVp*TX>N}BXQo)`{)u~@vahS48b;_v2pz>lvHs0e07cma4db7uV*)p8wf2CA9F;iR?c03d@-yhZj0oS_y6+-Q}ZM9Mam0 zuO6KhT!zDzZT4ks`}s@L?Vmz5wtdxtNcew|IWlJKa;jT3elyJ!C_Fy~fByC@4$NuF z`*K-~o2`IJDGj(U-B!u!)k1>gx8=nhosQs@BAwO9YVq{7RY=#+Wxp@$=|(gxI(rFS z7PaZV(>F*4T}um&S13z%22CZ%9>Wkm+5Dtyj3VK;)(S77;e?bHa(5Jqq{~H|#hHk^ zba**Jr7grU(WAx>J)V>AovZgKkhP<&_aZnSlXPv&larw1?+ZdWxLQ=2g^~79dkty5 zf;`ilmR)|v|B>w7u4jMzRUv!6=n%zwm2O=_B0H<|lKkbc|2W51mB>ImWv&TL=k|a_ zgiH>$-!H2VoRHxYio~NKA)yibu*7psK}-gL^5;p0BnTV3 z_?{r62!jc;sH&;5vb~QRi3SXy{Do1CR>bq0DFx7DyuGv5eWRnLMi;;FG87cJSxK87 zr5|O4kjY(oc*ZU$7sxy+YcFYkeLC;9tgpgWzwa{Aq-eKmwcWt22yCU7+T*zx`?MA# z%8~z0ul@1re6TLna%+3*B#dnK3fJMw@vwhKHYY3(*qbZVaGY(S<-_UM{*tzz_$K*h za{}y!ZwHjq_>H6(CT)OP7iQv9X}&~Ixv;l-#3q@Ir3^b##J+j-ub_4$HcVw}&DaMb zovc3*6+N{$Wt3E#t!gDM0THiWJyE#Jma4!Nn7r{D^)yHjgUViDU0Rg2*!d`|0=MYb zjx**rSp4OQIC70~H!->8Cm)008CoUESb?4E_n5(~PbeN_-qut?%7UBLme0*(UlbA} zA8WZ*z*>d7eQoLUXO}>04>AfHS|>RV9^5_Lzx zS3zn)K^0CNHJ7lA*@f3ryPLbGa$VSpU=QXq2WMO{R=MQHR~u`l@>G`jiMAA-6STfO z>X=Ox_wq~OVJaKD;*10!ft@$&E>rML!*}F|je9*B@CU)uv z9PLeln+TI+H_FQj#YekAUc|xY{I^5xU}irJ-e>!Qx+gRDo$s~@Ot_TU4~5C-1`*-j7#^*6+CX2lu z&&$^_lIqFuwNv$)%Y$p5;EZBxexOk=SR%u<$ZyjUcV5kzX2PhH@%)Kb)GH~wv0bDB zj`bQ+b?JScKqiBM62{%toibfdpQ!eHjbo6!;F z%U0UeeSWD>w)GAF0JlF82rO?FX*|PLsd(*Yr0M%%0ixJfHRJ%)j{zBhTBuWO(G_s&D5C& zhBo;;s)0Y#-6M4+)wT@Mw3}lP-&9gclmJbeBFOLGT?EvBZnBlb#}_T>_CJ(+6TeTM zo>O>;Np9hb;)eqBXJz4MfbGr4_c$B0tKd} z3jTfxzQqAM*!Y#;J8&fmQK&Oe*_I;yKK5q3yu7eseuts8D~aFNOQCFn;uXZy1)Z_X*JZQnqwAW)b&;1%`!AhSKy2 zfXv`)@T$-$PHInnl;$Ul*6+oLK!!poKgx{*#F)_%*-Wydfsi@Ubo=ZTpP~Ymygr`K zTjk5H+Ak4wDa>I#brUkr^R$-|#^*t83)Mop>6~7jjEYueBd&s_Z_0jljQohBUT7^s z-a!Z5`4K*V%G>VD)LN43%SHk)6dHyXR-Ud#640g_PW$lmuMYITmjRyz`@du0o zUG8G_QjwL^AL#-*vgc|%@e0$~`dfc5EWg@f%JittRw=Nk-q=)a0vlb%k{`?CLMWC& zO%%|t{Hb^u=5Rqr(BAI+Y|Ww*(iDk(G*8W{y|o~HSf<;N(B826fh*Hl-Z-pn^QJka zurOn_LZF>41k`aHQgX|q!W!NW6fvSfW}Tf#7Dbs~tJFs*YYf;x$ZIRSR8P5c57cIe zIwf@uq2qw4F8% zs-70oZt*%PfBOt3qb4I^pxJ6pRsr^h(y&oM4=3$eyK#qG;di*fO`4lVv*ff@3VHYw z1B0bx)`#)h5#^si;}ePYA)d7O@;Q_HW{j5b&)sZ(%-6K$eOstjz z3ly1}FV?k1m1b)Z8sw>8F}=5pQ|+z7E2Uokbd%x!3gldV^OOmQ7{s;~;Y$75W`{5Y zizr&h;@Opz71fH|2w=_lie^qX?bO#{{bqru8$ULXMzoGZI|TJ|T%OXVO@3%gW{b&~ zaVRa2!-G|Rs5*v5-I#X16XNYs-mo|K~9{0PKGmWP_|*uTn1?Zf{x`wMa%_q&f|B#Yq}l)&vy zTE}j3c3o;OG{KIk(t5#saESA&SJSS3`z8~-bl|auT^z~03>}qe*)mhTdDae;I6se| zn5G#Ir9>huZp;YZ5l^2g2x@eRlT1*kB4@US3i>%9$qIsGu1*VzVWF2O2DQu?b_IO< zSL-R4Rac-vSJjyh2V-9C595j?f!&Y=2MHj`0yufwPFIxHtO2Wddc}L)Hhcisli;}9 z$yxI_-k2$zCILw5qmUf4;+g0zg{PzQ!IR_#eCX|FB{6lTK&CCc$;0z4+gIBzTVtxDiY6M?I zkzyqyYY=zTs`{0EZbtyZ();TPh=5J;%}nRB^&VpU{-cPyvy-!s9|gQT4a{hKU1aWF zA9`t+v}o^m2y8iRwA!=Qegy{G-A+%EHV{P3k1z0CuqW4AdAhpV9y{wGIqk`h9Lw~H zWXHVFxw#PSl_yreN}f@=2xR;2+ck#uhx(GlM09Q>LO9}y4{r+s)H@@s~DRwhg{p7>wzREEQUt4E# z%xOVJ1$6;y4KM9mLt(H^0ilolK;og6V-o!n(IiGm(bz(ADcX+~oY*?WC>El@YML-Y zs_EH-kF=(hd*svi7Hyd`$bk$q_B8^1;qjrCkKxF?H6l2xw;EoS(Z@xTBWy~98gTP% zAN;sjSc`8`@{s_8v_PT2q%?=a*w7BhobjHTKb-TCrx>{Ho%o0Kc@-T%zti8AZjhStE3 zd1VO8|K9j73xG2AJ5(^N=;(fU+=mYcCyax>$c2QKNb#TGD3&&*4B<)VO_HVihvM^! zW5f%}%w#&f+iy0%(s;{HM~iTnm@c>4F&WuQhq)q)W0a6aC-euq-pC6lm>%Rk11(%3 zM6(f-wH3s=l(s&9g#`*x_Eae13h;wRlc+3Cg}Qjm&kluhGe?t>sRVd6s6H1St)vM= z-dz$)6hAJMRTgFT+jJU-5L62%u19mZ3YE3a;}aIvq0n#VSP&ZN^-MR3QsZugR<=v2 zpMlnFkdH9lp&Ltpsm6mfzOz$64jty{rk}6NVMco!)~;Nuv06Q0UkkXoGddp3W^`WG zw?Dp#rw<3@OdCvE{_ z&j}RerJnq5gb2D<9vF8(3lO~fN7h~;(>EY==`q%M_m0qB$vUaK)_0d2M2x4`Z%XDI zq+h)r2zbz`sA)RFJ=@!Qll_iPxu#LuwjiCuN2PTABc?U}J!hJ#2u4Lg*-a@3c@GJD2 zR2H!i@{WPnl{+gqhYn}BGfrRqwmg`HpW!Lu=j1oyt9E3|$kTVrmWr@)T*qTROYzRM z=m(>|`Zz1uQw4o6RjAkSX4+I1ErZ}w(LQH7%IL_wG^>i4=GWx=PX#173s+Y0XfDpa zU!H;g-L^xTkaXLZPs{SCb#AiY38#eYE-=_Wi3+VXrWf~vdDcq)HE-SHe2@L5zmx?~$qbUtZ~*l?^$W8ORPBTqB2%2 z-bJtv%DC)*e#5*VfZ(TWJiUe8=gb}VX_4&kYfNIV8R5n!`jS`O@(3>5sSjAa^IL-M z9r0NP3+qifg8OcdPP2cPjcx{(u1xs>y@dqz1sj3?pegZBPH#JXd9C9M!jHrK6J!2g zofj|vY+FO1OXJ<|^k0R~A>R|n*F`LD)6)E&i&6dORd=IKtze*NM})41sA(%(ATjCw z)JKkCN-tbHHq20S` zzB6iZv4+&x6D_?)H|=7xl%EYB&~=M(gOw9*u%|0#uR;;S0|Ph=RgvZWLq8==>cQu= z3`OvQu#P}pVnVPj0!26ias75pCJpH8aGqZ+tJ*tw<)BA}HMlldv0Gdz_R?$fu7E>X ziRzxdTT-~o{{1FAXlSyW_nE^dO&tujNr{)9{~yu&-ofz*73$1H{2BK3UTBjx6qTWF_h-TY>=^%9RYWs8Gk>FFX(`Uq@~ zPg8U=3{RXS9Lui)JUL@*^tlVwOd)-TVo)uFHRiDW!#m7?&(M@aGA$t+H6_zyjefySh<}p_N&MM-rxN{yl=rp^6)fjOD!agfH$&TRC#_r8zbyf zwJFPfACCT;P+}jhVoO#uCF2Fk00|;@@Jp(KMx(VR7P4A9|3UQ+j{w;UwZIpXG zrG}3dzD}Z?PF~YQnUeL=A_2IFR5OjXU@)cs<9AXW_~Wfj?N6ys;T!#5pFNAP@%0#a zr@v=VgF#P9W$xmVDkxZ4pl)SLUl+`yw)4)7`M3M8D2U`bejFP@C!Ef|#LXs%f2e|l zSe^6F+%3xtScv;0G80Nq)jG+;P|ls;hRYXPbFWBAa$*03b}Ikj9Vg$C6IdbXt#fqW z*e4{EQXo}`+Ooq~BgD{?=<(-TX^A^FKe}~bFFTCiZmm1e_)69#3Tn}wKxXYV4D-Pp zn{gB;U2`;@O#|-aKM%Q{kf}_RFGW5fewQfNvPRww)#^Zpxsj6W3szj2!cQzi`6B*_9z4(uo4yrDJ6|UfX3^>6C`Cv1s&1O?!5nV)6?>3L zpb)t87w+}|&<^3Up8ApuvAAd3E}1x8dj;n_R}Y@u_R*~!JwuEHRVfu#*3noF&G|&w zdB(lr;S;+srq}19u^-?r-hm zH|px4rge0`{Uw;u%KebAl#--jFmdax9A zvK^t$h?`We@i}k8H}vgrJbF3?j4_N=9{64}JQzgJ19D4F3H68CQ6!XzJ$a0=oWue^ z!H`|=Z3M;a9j)S#VBSjXKCS?Sn6b745J8ymfEi0W^PecrStj&Y@B)f)R(|sTq;G2! z0b%Zr!1?Ir$7d>VGGDfRk2K1+T`rLz z$v3%?6)B`w+C->$cbtaeqg&^rs@C_berJCd=-dbqXe z&vtx-LXa@zaDSyCGv0VnK+C0Pf*YyWMGbIzrv}EVrt5R!0IMwAkHIFXQyCmz`oHCQGbGzBt6eTWE6~ zl!*rFmMyjYt(TXLJ_uQwYbkc?F;VaPQ3ruR6`fDPW>;O}E2>L!V}~Uxf&(2R*%pRQ zs;kQl>`qvn(!H6nRNTUSI%lQ%Ec1IuU)Hip_wilS4f2NuNjhYQz&W@sbfb@^C+A3r!LY{(NH1WwGjO z6qJ*kf$2i`XCY_E@yfqN4p7vD;icfK5<+{eI`QzS$?<5uF9GH>w`*i5YuqdL62dc$Hszmc4JrDz5xQ8G+ z?W30ct^(zAi@M1+A>ylXkF>I^o(d2PoM)AJ zD9p~XJ*PS?8a_stdWXF=Fs-Zsxh1zeq^7$4B$bIdETsd)6Da@ib*+Uy|6%ulkH=O= zQs=X&#ucW))bCql4x~i5nO8L&ixVqGor=+=6d!-n3M5;{f6VC_=20$w6Bbf}5Pul5 zNE<9Acfnd}X=1gbORM6NB491u74@l_|G4dZHBnj4zoVlA3(ej`ozZf?DYxV zqCX1vBvStYcKLS1l~P+RZeC`!{c7H7t$8EHs&vz~ZkVIP@~GZoV!DOSgx$hUs(}MnN&QBBitx^T?i$*|8zM*g;_pZ0G4*@72*bt_!@UIe2;ztv_18_E*MJ zW_rJVV0{ZM?4^Ol_2>#;Sls!T$b^7jImWc zk4QkYxO&*iuPv1Rc4NdNcY%p}CeVYP?xCF1fO#@`R}SFYI2^)*>=aoqgoc`E$%ROy z)L`&E&n-q$q{f*!8As`sPoU@58b7kU=L|c>0v^7);&uW7W}*0|{N#VyZ%@8{s`e%y zQK#sh?quD(BlZr|7(Lm&nN7z2F9oD+7u_xL6=U1(wmhojzi0FRM^YWXu)$hJV2oJT zP;oCLs0&esR9a#rcEQPduU=BT zp%5cEgb0E&p;Qj>8DbQKTA7$%m|g7LOHuUKy+KMBmeO$XBOlTfx!j|*UaLus0F~yk zNyMd}oZHofRR9Ixby+onv78u-twaE3m>M|f-Xf$szjD?z$d43NCOk84rQr|_go$Or zxk18=`q}!!7yrD^4UpyuxvANhSJ;7c4>I`xQRQD6T33feX9Ipyr2frjXF!XFUM7H{ zHNoN6+qj=^tq|zJNA`4}XEL-4=EA{WWV*=+{T=+UZZbob6Fi25q^kFpO(G<^WFp1B z$6Qw^{#r_Z2QcZ&4(A8I%8}H6*k$GZ65wD!wO(2M93}aVfplxe{*AiglU$mZD(5CStAgoDS0gtLa<8uR~&37m-w&z7y_ToFsW?LYRmP>u^d1 z)Rtk*K$|ZEvKQlp3_5g&@d1jIGaki358>zdVonLRZ2Jevr`RwVq5*kfha|gSllPL@ zmOc9x#zK6mL-;sq$?xDr)@eY!N~KrVA=CHuC9wJ~88+x`N}Rtwz7(yd-Nyyah9b1_ zk16W~y`FQLV298F{R)m60?ep>!i3CR@52J^c6t4(KXLAzbx3*3(Y|(Rrh=kps9fq2 zh>IGw7MUE1Iigjca{|AZZ&|N8J5 zlOj^9fV$qwMZ|yQ+$iZ+?Yf+b#8c&H_=2FRO%GPh8*!Rf;R$Ks z?vriISl<|ynnj#_>`ZdU$k^?UIOjdR>-Cvo6fZn&8%$*t-!?DE!-hCDq%jg;3>^Q& zg`8{Wv6OSA{=F9_@vT%^+Ec@(U*KE$bI~)eiMGzgEs_~RWAY4#V2-4!;b!`+5XUy$ zxJ8NPDY3YOM8JrwP_A+Q!yxiPJ>^*L=XIzY$AYfiqg+2e+fqtG|A?f`&)Pd$q@`DW z$}l8Ttv&@N!=|RHHD7HVh8@EW_l{cE+L)VRiee~>m3{W}`69oO> z=L<{yc{~WE`}UC!bQWIH?iM5NjW$Ad{F=n@tK3!87aLDX(9$~!mdjBw4`(%oyM3N_ z&mn&Chn6!M;ZtDtSbF=!RruNT74iR#NL z+48OTj{G6!G(T;Zu~*h(tkM@_;O~>y02QhS+En!x-Sb~whNZQFByD{d5F(a&0p6pB z_^QV_1|RS5h+bbOzn1Dc8)7;o^y0(trGYntuwRANv|vsu`x{i6 zJ)jhq{FCPpt);Vl0wGl=ixxcnHK@x=mUAb<-ziZ+tl&UnW!sMA%doCP;jXHk| zAah$FCu=u)3W@d2!L6}=jgOi|7~S86>M>c_Ael@jt>)gYnhx;AZ zo*+Yy3H_cQjv*MYTZ^(>1>NHJu1^I1;_r%V1*NN719ciLSPRd0My%(h|fI!e7Zu?5u-z2*R%a52mCj@B@~WZl;rPudc-*!+!m zs*VV!KXWV(r>%WMY;2R+Zi8oCO?Tr72bFK;YB4}wL?E;Cx(L4rz#dUCLBur9l{?CJ zW^kQhK}ivWs(qaxR6G|7L%K5Tn3vgUy0CoNjXdJrRfdMVR@vp_oHwH{VOqYY_joVg zl{qROnyg+^J>S9rkxbc7%ceN|DzE_5H-ZH+5gGy4H#>ogM%$twn&1P|LEWf;Y{A_B zzWt>w-pz7y(atQt~BvVvC@%nXJX4 zqBaQr3od|EN6v+e5ap~RNUgk&&t0FFmo$(p2N6NW|0q!Fvzfh15JVY+hV$Es~IZ$+vCnoSqiiXH?QS{9at1uN(;Y&iAaXQ zT?sG-`XF=4ws3Y~W}4glx`Hcup=w|0OA9O5DEX}%{R*yZCov=^V}wHddz*)3-x7Nmzu9iEvwpz>XG!^ zE@XU7!nbc^uu~lf=z$jFJ)2z2Tk)3Sf{YME#)( zBzxo`!3otZP&+}%lJ(MCIwmFHptQS_Y86omnhdg6Y{!lBDcnYuxU`Ok{S+q(*G5c9 zhNqtV?p%+M2yx^j&5S-^{NP*_?RUM|*6?Eid-v<-TZgF%xhO}y@p$vZQMr2qmPtc+ z{k|;d?cYOEnx28~W-TqH6F#r5XB7C=c~Ue1s11ew0_AW^)DKFt@p-ZCN|qRujOv!0MDkV?!( zGCH$yb-3cI4kzikR)vSsp$GY>-19f&QjyVJd|i(TrDcuO*qHx>ShxV}gtvkq(9o3S za922%dT;7iNfLk{oBBxLLWs zIXn9gtexWdUAKdZF&FUnaF0cEPCZ$bKf}L}Tjd|kGXb-@BE$ty5@#t44JwX=k#M}7 zKTBWdU&t!7;b9HUK$uE_SuK#SRvK15+V7@R`8>viJorT`qrZHa>bfxAsV#8_MGj- zc3ok^#AK`NMx`D&VXi8Pb2Z}q)Cl&?OD$Y9GS5lJ(o@i3#w#FV%We6W-z1#BK^wf#sr8ElwkHYg13cZRF7 z(biam&9RK6ObFqSWks6aY!UtidH6tDp(u;8&ixOFWC)gKl8vXU=(;Gou5V!Tk8pRr zQcs2VR%yO~!b1;o1EKS&j+G|xeu%&vAfqISz${l|1p32(cR!vdCz~8Wf6hC zg&-YU8K+$fcF}4S1*lQ;rN0F?smv1&qNzR)I`5R^+?MTXY z;SLjBXV!e?zBkgk;lr3vUY=+;dEn)RMy%7RfGp=Dh1XX6qbvo_4|y=cuWW$NrzGsu z=+vP(3JEf*t;VrpuR`bSPN!Wwf3O|`Rp3z$G|KmKrgdTu-JyT%x?lPXYF2Qjt<$CV zl;8M`cs%2jX9a7yd9ds{K%T+Y&&0*8U8?jE8;z{z)l&=jCb%>m8iR+IX|T{{HT_}i zO42WXMXY2sHX1PRA;9i$+#u9e6lTS zu(qfP6+UA%4&5q3IKhFv(Yk?$y@7gfGu~{QBSdA}I<&St%)xC?dq$aPQ`oUTlJb$6 zy;rA7#n8u=8~V~U8w#^3$8L9k(zv9q_?JNq%Wxbp!&53!qazf^8hwi`Acir2S)(B9 zJCe?{-!2uE{NUO|N~qENP~|KIyVgkKdRwF_Y?e7E2P|J58%+tb+j;SQByBm+&^JrM z8T0)v5K{TSSO8d!T+;^Kjo8MA%Z*u15G*Z-Q&j@oV>(5i@r~oB7)j7=>1(}^m{%fd zf4t_D@1mHoDBC6@-Hq^Hc4xc?JW)w9^}(+CgCG&3f(Vz?3w+9is^_tON&_sl$mj-p zG&=SBTDU38%cV7*=+Y;ro*8uo%CFek_?T8G2b5-1Z&TjSsA@*$k8ZN}gah+fv}Z+f z^SjISTdW_g4$bQt`X4}QVm3lBPTq2iUb*<`9hn2fE9FXMXIh&*j5&#&-_}P@4i_={`Oh>Jx2J zU>;ipc&nc*QJr9)7Hpp-U#MzMe32L(;3s$Rt(d^M!DxY)l0waVhre^%oyj}O;%qsy z8DVcyzHBND@w-mD628Z8w3H8SQuM8_GFVJa34Yj?G&7dlMP8Z+?y=3j+9{zl=IDiMc>PZv(toEFDIgXW z^)nv=v!VWt5~ExT6|QxtY68!u^tR?(vLq@nyJym<2y{|LaiIv;F1@{$%85@RFg6;s z?bS^lrg9R^shzO2k?$g)2RQ)(t^F|jOqZIr&D3DCulLrkB>NsjsxDP^-K}TOv&})D zdz;2NN;S{QO?M(4*Cl8B^>C^l+N!p3Yn)J>{k$;9@sHWc!3a!$6e54%R>%x7Ct-=b z|2(3vUcy5uQ43UX7iX-Z-7)1@;uR zPHb=GzQzi?AC~*G>U}NG%Ox(*RhzQhdNF#Q@_y}G`G)^$s820U&tRz`-_iW^SD3ZV zhvR#hVHuHHI!5g_`bpMzu-2&H&apR+$J-2cn&~QU4LVcZZWht0;WnHei5*UgXZu(Y z;zq~PR#T4iKSPUSfpj*OD@RIu(8_+-3Vi>o;$kUcu;E z@PZ{CM{Dp}>ztTRR^c#?Y)B5 zrxt?umfDi<%ov1jR)E>=GgXI45erltdN0L`=nkys!?$9++;^GgR*VVnx7{AE}T|Zw>p&p0>mW24U@`MC4w_(~N#h&BPoZiN|&EkU=zqyO8n-`jbzwL*zNM_sx0((y1H(ww#Ul1^l96>oicAMrDirI ztP-PzPfP_RmuUW%hh85HJ3X56 zw$FhbiCVd?Wki^_CPFCJ8ndpn(Z)@ZKs04Pc>U8~Npy{#Zsqb6%x@b&=L9_p5P{#{trvp zeTBY;&Tcm%&7)U&%+u!$I=KoVOcTqhJ#0Y|$5!FOuXM@DRcx#%!&qZ-_zgZ8&ai!w z$0L|;2rvD`rW1ps@V@lJ&E|!Sx1V1~q6cx+ge%Fayy}ucu0}Z~bq$lh*T&~ShH5O{ z3Z9r<#YbVaHryxPLtIQai7yIojEJJcXcm>_%ZT2#p!~F=*+%7^Rw)j9wT+PUciEHO zHNoNfHBuhIUk_birn6KGlN2We#x`LGGUowqbSv%4NP{D8`*;7?d>K$6BK5~S(^aswSI2RGe;F(fA+1$v{#eb&50A9M`TDANC>;gS zcKpq`aX1Ph%0G$gY>;;8x9fs;hY2&Ohi}JKSMbhEdrpoxCR_(j>2g1|s%|?o3cIO| z@A=}5EaPgDQ|7F%2DuLL7vyi#;+x-|H>41kBF2y?7woKvl^%Nppr*`{6jVtY?0MYd{q!TM|={4f;yE|WrdHEq6 z+bXW4v+diy9U*I;MhI*3#38drOoOrtzZk0o@>)v=^JY_%nNVp5~G#3pYXe@Ictj2!h z^lQ~Hx(2ax*>+7~G*n`Lg z$ecjx55M!U9cRYL5mck7n@#FxE?Q5k)cNRPJQu}jx@y7T5FZ{Ty+T>TAbH$G0=o4l zvYU8QW5zU(&Cm;?tO-f%1qv*>eOm>Da`_!vGW!=(2|f94`9D+1|DQtc(tZm-%HEN3 zZ3K<~{gwZnu^|dRN^v|f1hbXDmbS+RwS&UUf-YlEtUOojCz`}X7^FBz1N!i~kId7` zCUV==rhQk?fx?s5v^f+swu1u1VRG1m>DU8CJ!9M*sy%b1hKuiKiY2L0!Zj;2?bJj^61(XWnq}<6|2nGi#VflZINu>0v?mJJ*__aIUJa=!6*LUR2WJ| zmotm{eVd@&(8O3jSPCpz| z1)aRdd|)iY@l2WXj;g!|PwRc7_Q&-XDA^0%7tc)owuyK`2x=o!xhM6r`qDBd5_H_Z zB7~8O#s0n{li{j!yDnlXo4JmbDz2DI7@fXr|8KX+6r1K)gE7YOhd2cSMY+^8);CxD ziopJ>+I9s7L4%fNpxk66=^Uzqn7D6fLN|?2#e+l0F0KO}w?II#gS7DkP?rF+tCmwy z(J}=rEhyr9RYnV;=;N19PH-6|dxg)o%2kVI6q_Vf>_C&ke4#n8})6-rg`?8i0hLi0HZd{;dpaBt0Z zIjKyVJk$rPh-q!yO+ni(hS6+%CHxh0UDV+Qe3B0AbmpanE&<|hRmb5TB`-LrNTMX~ zI_DqBr;5gM;ewp2UuJZ~$|xI@`na4${Np5$p3F4cqbUooyf)TDnP@PljM`mWVY(nX zmJY`Fg-*w6!uySHwaQ*WgVKP3LFvvZuK0kb4@}VWSJ&F$A1e7Ubc|j+peSxh3n~DIY=E$`xtrM6xd7c!~t^Fo!Bgk@8 zC3G+FW&J5r#;LcSo0ako+X=&JK9y?LpNI|`RKlYVg{;)8IEPY39xs>WZI zf*~NjDw(i@yVlRn7K0`Q!qg_jo_Cpa%j!RV?8cxU%un0+RqGoZf05<9wPtY!J_rf0 zwLO$@f1EK3U1Ean<2%jV6%2Yz@j}xf17T`pYkrn#zUY_`N}oosiGtjN0{SEAi=x#H z!NE+fHP#ITK}}$*=b`M}{>HSpY~1b%nZ*qBsBT1G#$h|%f)e`QhddwbsalkAW%||s z@@Z~#CMO}Fqe+VNWW&fzXGI!bu^qoV;_17GvA3+_e=%6yW^!GrB}cSU2g!tNt*?9n za<^a*9?JC#Ki|1_pKbu2maP?ziv0CXk8+ywKT!&b{BmE~GZF3~}ItVfTk_ zrQXs3W_^-xU!9cxTfJSSj{)4(9ep@{1TQGs{|C=234Raw?aaI}F}zGs7{C|O;4OC$ zkN1009^>tY^iiA8lwuY<4UJNdW^Fv|S7Sc!MX7I+q-<&kyNdzLf_z-n|Bw;DUZmV> zB2qpIsHmdUUa*EcdV;j1HOarNVbFY!@umW?Pg#%0)9Eeuuw?9LY^wuAAVsiUs76e+|$nM5>RwCPV6P6#bGg@lAS&O8xKrT|cR+ zwOL3t_Nh|X6Yd|e8+~oXqP=Wu{DKGY!Ii;lC;U@Mc%RKVNtj-4et4dz)$8U|1iZRv z*tz(iA1_F#l8*DPOJi{M2*wn7uQ!K^4W`X2iluwN4$2F&=DDU9%eE_AbQ(HSR`B7K zA({cphDE7u7U$mMU#!%)1d~Mocz~~0VCuTxG`q#-YYed{XnyF%JPW8wF^taz6K=J>a@yV3b?*F?hmREfd z|0yL%Oqu_B?K|>^(pnqheoM^*N~&3nI!^t+SQsrN$M7_lZTi7TY`&FKZT^4(IHfDS zG)(v}!U?h}XX>B%1SR0wO|r@;0`##zbTy6MvV*Lb4?y=#Dcp9;w2|At$!aEm35w83 zT)~=a1B<K&P!U%!(^v)C?jaa}JuzA#;Z00TCA0|~IP zE;i|FB7p_IkI|WCvF&>jp~%l8o$gRBdp4{v^hp?t!a^LaMDdMT^!+gK)Y>(io20k% zmQ#j6dx`zZAcOA@S|qv+*X>wQ$(Kj*oYP{v5YsD!EQR|!DoWD%Fz?qU1xncwd!6L> zD+NL_vC8Oc8A?UAs1_0D)q~ZOB zYp6JoM!)G#6xu7%d8b-<=6_w+!bCN)b_U;|g)fuVTfuxJ{)I~}QMt_B{nWeFNRycC zRSH3qLlc;_Nu>=I;qkn%KKwvD-Otad53!(;GJD_AV+{*JRnFpNi^D;m2OsEw^+D_& zYx-@%a!U5l;DcVN2a`XxOu`+`v@5+8uR<5BpDawmF=^fi+lU^g^ii|(bysRGu?6Cm^(T`1N`O8D+{3ago^{fb3Yn;xD3b%Bi`7F zcO-7VJpJwCo->ZSbyVs0^YTGd{wJeHfoCsNM83D+DVFqP2oW-7cy9}*-Sh0gas{=X zw9+{CY!wlUou1<;@Hb(*orkYyN5g7avm=WY$*jp}7Av`TYh%L`SunV|B&zw*CdsPm-N=2zvPK;7zc8zMq8q|~GRq_eO65oM|u zsm-76OW`M45Puo*{)42!eUfd`6VXhPj8~!&rSC|)sdJmt!rCY>r7%{@fT^+avr-@i z&g(V8D)S+`Ai6U>i=K5+aD)x)uF}VjX1P-#P}>|QVXVfG%c*wQPl~-6#Z$^sz#^C2 zJ3yjP{hnGLN!DVtS-3-WXuLh77C9m5A1EPo8Wb2a0JkavTnS{?)g1{r6P58?5V-#A zBbU3>F$5^W8BrvtuDrW|YTf4AP|zl4Cw|sUv)>3}Qw`BU|cYCy=;4vJV=7BbRSjICjY0zBZx#Va2n~q(ZgL)93o@hgV2<+F$Zi zr1}DV9L~QXSGL$7yLDl{8g4mLE1h+D1+{+oERvlJlU*+2`5(=e{R`r73Nu{g_iemc z%QE|WE&T6ASsq9)?Nn|zhWbt;j0@IvC^}AKV)iH9@Nh=8(8LejSHtx@TN)nGT|kio ze^5|s{d|G`Xojz!QTa)zwm`Jlsy47yWkSmU} zJ@eB&6k2>zspOhg=VDIy%DRTZ4TvrDgwjGjMgeOaq^Y&|@iq3J&+Qr*TY*FV*7ZHw zMTEvw!0Cj1+0id`Vi6%nH~QEz6N4`R^#*AolrB(t_=rJ-tMc!7kq=pHt~gmz67LBI z)M6qR2F%~%m};N2D~(S%ZDOs_>=e%Lbo81C;qmGMinsTn*d9#SvX0cB9Xqx9YC8HU z#B!qHCE?9)dk!H7Yy&#AqiFGHr+Ypm3KcpS=q}H({gh3bHEyTx=ANPe;{9^)0`WR_ z><;^wHb+lA$e_t26AVrUU-^`^Om)|b>iP=S9FD?{WBa$8sr*h!8SPAaH|lbj=IxZK zlVh|{4j$PY)wvlygsy-dL!hrLvgn6{zyTW>2U5d0F_MAZd-2Z2B3)*ZpTNsmGN2d= zF}IFA|I~}7P}P|8$~SIaF^>#av~oQS`A_#I8wFRxLL>qcqccvdd^@}5GbdNlEa_H;u4KTF0_J{+IYPD$C~z(vaTIdE6lle{)E+kq%x_)2GnW z^r+TAEVkPBETBWe9nLs8u(q5=S2zW_0kWT**_t!iemliVGBFWGxUIgsu z667SbwUc~kg9FHTD25opmv z!b~buMs7nH7`nN*!3pWtv}00#0aIMZiOOOyMe#WJ&!yZ}K0u;>>iUXyY-nO}^k-5| zIN=}vS#x$0K|*pc_}7i>dU7z$FvvgEA#_=ZQKQNg*~9=g|8e5=a76!-l7l`;ThjMx zf~rC{rVmzWZH$F9BCCJHq=?sf^gjLJm7W^56Xm4^yi)`uJMeWmuCXG&qJ?7@mYi4RWY&?C1)_c*A(6ionFZU=s=F=A7 zug#%}CWF>Vg+YgWz&6>5mE3UczS?z_s?}j{W@IcJ_>IKLZ_BdbbH3uW<&1Ck@`xyy z^5~3cMTP2x$->g^F+u+`bKGv+-_X?7WCd=Bq{{(C-Rl=^n(ptWe;*9-XFn;ytCT2;a`dp-< zlOr|}Vd3G5%uPy_JptN5( zQI;i^e;4gSTuJeoy3#Jx)qlv;EosT%DiHQT`k4qj$c51;%MS4?mEb~&18RxI8#5<{ zozl+{NN$fGK9~`KhToOC2{r3`ZRwFZpeB?l(Uc?Q+dYYMDX>N@h(JXHa1%bMqM7xQ z1~lNi^fjVU=HoxG2?~AVu$}nW{CJf_v2R}A631GyWvTB%Vqrw-K#!E(SCeM=z!eSc zNn1B$1e55qrzgcZwj5DWNhx#f8klZ4d}TwpI^kqgrp^CQCU$R^dA>bWiO?O#8lPy+CT#AwVR^Li!uXsj_ zkw{CnPp)GaiSTxScx81zYw_tq;P8$|hToZc17o^;3sltGuJk?HK(s)Gn$_nF*6=W+ z{eK<|jq9v9P~3%PY@Y6-vkB zNxm^%J@Mgnb8y*hoDW@N{)e#}@DwPZoR`90P^n|YuNXyWi`${iTLWSs_RmY*%WlOV z(x*Lx6+6*Do>#mx+9BrD5Beve9aQ?{p{i_%4c)z&UuSOq(g>8_E5BA$!@2Fx%JBh>kr<^*31bUI|mWL zgL*t7GLYaWWOUCo;Tl_#6>e(xRhl{~DmiaNkOs4^hwiZ)2}1PuMW&|kiBV}nK@AUJ z{?uwZF!#Ev4z{5s5|}4_4Tq_Iy`s49^6r|`IK{p=OI(xt2lUkX&mx6%KQLtx!}={u zwbcmH1|$5+(S7w@?9e3S?wvgo4Tvkv7LO1fw@ySz%{z7~^6?(l;AIn%%_AehoK9PE z#f>5$6i4T31^M-^9^$?~r3rZyOM)lFdx_&6Rtk#+}wsbl?ni^ZrRY#eQdS8QT9q|^+jAQ<7d6p}9W zQZ;^@&}5@PKdBy)C4~^tK@37NOb;rkUUCB2C)SQp8ehqs?yQ4}u`IS)NJs*vC)q<- zB_@V3m+0Hx_nl+mfU`v6u${H~9D(`4^Vo2La-Phvu7mkLWKi6cemas$g@1Tv_^-ol zC6!^Ij2q>0N`4=I?|e$WMX|-g`wFhC-x?l)7p99J&>`bK>xy09c7;?eXZB|a%8Q_T zR0UQC-mYcuqQO7$moxafXh~8619Dm)w3;cq!oQ`h9kLM!;+abJSS*X$i7EyktOW`do5_kd$xz)?+XPv?bwo1;%E}T;7!e_WX*B<(_R8R}1k4mh= zX7MtC+2C8eNyrRRu-VYd$-O4ZW$N7EYd&Io?^AHTjBnXg8{a119`Hj7rvfp_591Td z2Z#l|ag~$fOot3VPc5rWQBa8aV};+Ku{gxUj(_c z30fmnCm-OjUf1!ihXaN%VFEcrp}-h1Oe?yVTi44qBHvDwOLsV3c3h9*yDsagyu{AD z%E^jd+>N#%GV>@>{1tfyu-uaL<{YWArqp09VWRs4{uNapvJ&M(-`r_M6N?9+L=xvH%fjuqdoJ|<%GV_^u- zg$oXm+dG z@sOY{eCLjFL~SaN zH})9d$LYSw&yEKuyA=x{!_MqMXq$|UWfRgXm{Ui?zwj^*g!^paaBK;v3(n;$psuWP z(YOiZSAxoq@1x=3xs>>r-3RG4n_#=2K-DtC7soyRBDRA7GIt|Pjzk@7%aRqj;V9k5 z?;rb!He?l>(@pU!dyxqtZ7Vwu4faoTnqCSa7?TB6rb?FJqGwn{spc;$?32n=bl%1Y zP>xAJ-HZ=|&F*TBR~EsciWa73L@c8=fRhzwRfXNT-BvE%0}1Md(R6%tzcw(UiZ_iH z&@FO<1NqRIpYvj~DlYnPz${|+!TTChzL&GkWuK>J}8`6nvxfr-zo zTGX+uRrSl4bmyyY+AZoI;BB6 z6zT3R=}u`8De3MG0cq*lG}7Ji{_%dEao#h=xxbx{@P#pm#k%I2^EdI^X9t7C--7o( zE(%A%O{BMg}XWJf$30?JuGOc`0uvC#FIBA zJ>7N4{ufXzqQCxQ8?O!#emOt{WL2`}n(iRGOeqUUV~C zcw5Z&>o?y7z1L#W)P(qJjRyrZc3oR7Z}gb@d^j9NYredr6?Gwk;Z|<`q~GFh&AL4u zA+A=9jZAYiR#zqf3fP&^!jLEAy}tvXV7vQjyfFPUtx5nXLy2Z_03Mpf7Z6fFbo$23 zrjvN3@B~xhED*G??IbXgDS73=eF{`dD$AKNT5s^@dXX6Y zs91E=k>O4cKw;QTczbxh9c&|QVMb{9Y=~1)>VEF7sY_O3KQ&8cFk08far%gNAkA8@ zk~xB`KrtXmfc<5Mi+}kaRj;GH52ExVL-ygpo}#%b6k`Ai7CA6WGo%p#2}tkGOyF4rVL ziRg0AcVwaEFL_kFYlXbxIk`;+g0zE~lQA6xcV!5=8ujGvl*z zb09^RS7znG!H7NOnE%6JVVJ?qJ{6n|$IhvxnrrvE%ia)X^{_X#kEw%YHMIn=E*3uU zGR{g=z!m_AR^GV)vN4 zkiw?f?1w43FzV*)dnv@=j44GlBZHrfF@3JCh#=idBZlPIJH!gCN>q39PE3w&^Y;k+M^zEazS> zbpmW9QU?oHh@fz|VV-w+6&_(^V*c-Rk|~y6Pu}sW3N)DIpi3B!FInw&^(rBA#e49P zVi*7n!haUSb)P@U!;E-uS11>AjDN)1@JVS=WTpcFygNm`A)tI7);s!I;MboKOT$O5 zx`dD7H&;*TE$hnbg2fdvZqB}WfjcKby)0s(& zfc1WMVh?}-_LhhbgY0}An7bL%u3cHKoZH-eDw6!o)k}TUxQ5( z80wK=5~o+a!OqB_k9h!)>&eORD_+&(Sc?QafO=;pc3W~#yj*B<+5z+gfCw*`x2F1Y z8d_R28t618)Nnfh0MTyd$9!J_6lOaAEK+k|k&ex>zTK8Kn#k1gt)_R#2eaB8-@Zn( z;PIqj8#s&jK(b^uFj(OnlyD*>Y%LYgR)W(uIa;f(7Gc8>)CUYyHIRMte)kPp9C1>mED zRjXDrLK7$bce;An3YxUNQ>AhNAXg(rOwRMzN-E-zV)9{!-KQgJwa;RD81FHVZJ7?K zzk1p6$kC7E(cvsqH~TkI`i6m7=pvzkbZ5myCqBq@;K`In^GK$THAzYd7V{GH8L)v? z_?{3BH`?P0JKC>apZFUR&1`QYGSAVS9Y-^qvNJQOciP9!Td(HMNTz};p17fIbr(Lh14edRquGH$f3`(Y;{oO3VA(SdTCMatbCOn@nLEpE72q; z$X#K<3m-%9_3m-gQCcu~HbS!Lvrhxnw6f{>dkB)8V)D8cQ&o|p$l552K0y>w_-B3H;U8If$Vb%GNuw<0nEM* z7-&cDvHNTl=>na5A2QR=KT+I#qi*ixGM!$troqBx2tQ_iMjzj+CHOW=?u*glVU71J zEX<;roz}@YNUl(Y$Lb{8-&GS(Bl>2(j$DCH!Hd zT?p8Ghvp62>64Z2DYJt)nKOp(*MpZ+Wh8X}nk)*9vv+F-ckZ`!V7cr9C!~k=PMW z9Iap+6{j+gMjg)}@yQ6AoBy&N&lyl}n8fFb_u?(uJsse%?Q0{0xeMYyaOMywxmUGX z0+IE*D2v4VqzNOR-)Jby5|x>qYf4iZWWYiSxS#=F|5UluTo>E;Va1h$_tb^Z3tT+Q zIrZ`LhU0)}Rs&f+W1ZfUn!dtRK>=`#DrxZ@nd}r|Iun{ddVmAWcp?7S!9LgV*0iQA zxYuIvyV3g#Ec{;KXzJZ}Z7@Qt0+`7>U*#pV4J=msZBLk7#fmMJihsOCCGIyYzf!;K zm6)SA4RryzY4|C#57^INN zxkBSz-6R2hCJsO#z7~T2a0=8FDA@xm zNOu5YHMfk@?eeiN;`#Q76@~-f71yjDoLV7wh9xb66qa=&C1kOU07D!2`a;_HhZZ=r zT%#Y{_i6L{-yc}z!++Onv42H=cAx5g^lLM{|NfmmXUH}F722}YJONIp=|mpT9k%s> z#qY!9K0#da++bYDfD#bC4-HiN^iGD^(!09!a0r>zAvwhSB^3ap=)~M$c;8!1^2hPZ zSl_CEa_Y34pE_2J3@loBZC6X}TPfU73_OgSw90;QPB++bKRBpeQ*4-hR5}N~1c_ae z=f}k-5JlCSSedv;h4t@10H|KifuJGIs8Mukq@Maz>5+bMMx%>#^!#I;N!U$lR`)6V z&Aw+8#afOMn{MpXv?0(KJ3Qayi z4|oxoH1mGo_0!A+yYFC%*J#N>LR~Ff%SHj;dw-ZMLMSKe#QbvrYZPTDm8#yNF!6hw zfX!pfMi#5IgQSci@TT_)&P&}BUblX0<+m`?@jX>V9!Zq~_pUF+%wde2WjYQ)ukSab zeQai{@L#R|u0kjdY_n>=cC)w$p{wGh9>AEn{&Mqy+`CiU&Q6{Ii~hj;%%T~KlL+nl zGUoh>^u&FP38(5qD5-*`VvX#sW#d%^TnQV=s7=C*6YAgcVhs0HVuB%>;$GSh@N1| zIj@(nJf>24`*qW+a;S1iarR37J^vHGa-A29%Eo0=spjvZV-yn+yBxn87NI0xl?R)> z)OOf3IW`No-W=u2b7$%WIs8Ri+Dz*f4T=-b;|d9qR_Zw^Nx7NDvlo&?%7AvZ7-c2> z3qj311~H|r&jwDHTg0! z2pI;rSU_4_;8B0+W~QA#8O4!;NPhhFaGi+OkSq0$vBxK;6Lp7g8A>7EU(5`H1yv2o zm#guDLB|&Y0i20hi%S|I*J7xANDlWSeER)+yoJ1i7vsB*nv9_C`|f(#!Ir?hhmL?s zEycp;1$!Y;cbUV9f(_w&_4NxC>MrENCq07FzovJ0PXsh6P-MP0ydi?*{2#D7C3Th` zJd?D`TmsDuhLu|>o8d8h5rgI3S;${{CT*3l8uT;psx2i|KFfXb&Q6Rp#gDh9;f#^| zaOkWWk3s5Yn;bDCEt%k`sglj07Cl&G3iCNUodZ2mZpnH-N%Ur|)Hqo7Yt|R`gHxB3 z%latTrHbWXIee?;!Rx2W5T>7nr8j1+#qCoy0*!tGfmX$zb3!F{IM|4Vb|dAPtPeXo zOSX=?KQwDA<6I2OPiL3JC+W>KmMa)JA4vM!tDj_iZ%!VRm2GkH-aYD+{X8d(93!sK znmNuA|K9$O?;DtTk{&lV6`VbJ&u8nS1?2sP^$Zbm;zfG;w!h8fOKZk=QxRHQ5`!c# zxmG)qGf&8}WtpO>+NIw%OVc89D{DT-hT5CBwmQ6P+Fe2smF{)3|TlvPwJ4ubhIDhn$)_$I-k>4Jf$7me6 z4K46KwU+tF{?PH#*M4E<6RlXA^~#@Wb26cHpgI|tzQ_S1H2&3IHryE@9gl~0ozG(| z^^k0L$|#}%VkUo$pPIKv^#=xwDaYPB0`}Wq&Bgh%Z0~$}zM$GKSQOscbh86>d^wGa zQ&ZSPuo@cwEOBn_HMT~-Bmq-#4;@i!IEFU69h$%w?j^OyZ`co>+j+W5IL35*A+Z#> zZ@0I;@mQ#r&q9u9m~E@<>E0(+5@L^F#k=YiR)RtEA4evfr*cgk?%AhrGTh(dT}(P} z{K|jyw&{y-E7&hD0_?@b1sR3N$MIGal0FW7W#Vq?uDx*YJpnQ?BPM*lJRW54GS;}{ zZHa%TjBlVQ`7kriRW1(upgSV@$?{F!I+Q=855$WNBE1y7tyOhPAwdW;x8mDKD00aA z^SV9ftNjv&K8!zP8m55^qHI7$^F{P2C^}DPxVpJZDXmI=o+VQ;44>l`N$fA+f<`xv zOV~Jwg;u@Mh3!h+E)QEZSmr%M;}Lb+qB=%++vk-ekeU~9ty-H&hpiVEI2qK|xnkCp9jUo-%E$U3f{=o;mBUZD=oIRQG`F@8B>eh=V zonCb071KY-1@pIl)60%gG`n)pYmaP+WOCv)F!If~fbGSFUM=VU^}ur@oWQzjav6#6 z!U7r=K&n0+BF?aH@qAH&Te-_5yOGv~X6bz^-P)L(VM$9?4VH@41ceP;ODk#wsybes zkk(rWPakk+{kHkzQmPz!uXHKD^_&I0qVtJRleV`n%jpk-t0~aw5TV4Gtv><<_uO~} zk2SyyFW}-r%?AY%${}Tit|a@RpcB3|AfVM!ZZr0Z`uUji;8n)-h?BhJJtZ~&!0K2y zeL8+zVe0t}cZBtF#ovAy#a_?u`3IgX@Cm9e%^c+QHD21UJC+qPxc5;fB;>Xa)gRk( zaG)JXHaoEytT`9QCK(iSzJ|cnv;R=H^{xlY=0~KBAo<6`vNr*32zJOZtBsG5(+I2A-tR;1U%&tXYE2_oWaC zMx@iylT;fw?}3lo1Hz{kN{%Kwe41FtDQ5ju`UCB;gErkw%G1tM5Q9GY@{iF7`IJB7 zYZ&uNGFCCCr(wu7mkP`Vb8VpQe_J}SIxlsWQLswZFKT`Cftv2yZT}NGO#L{nGlktq zjRl&u<`@_BKO~OX>TFy{PEA}6fz5sE2Je+qmfI&p{QbTmwyEet5FDT!0={fKu@1?78$PA@rXrqq$fsAmqhw#X{;Eo^b{o4Z z`so`3GcFLV*bId`JOh?Xqf06Lhl5Kjt6+-}+B#zIaJfjy-B zDntPVIFYX%1=;~bui?ByQ#j1g@%;u{`7(3I&}@8F=q^{@;W2x90-sSRFiPd^fQMhi zq)d?p&f)lr@#$~aM~Tp0Cif^{egnEulW5%*Mo{Bq8x8LlW}z2T%80Zb^>~Z*KmM0pY>jRV!@tIxUu$obUn~}3Ril8nQ8cwiQKjC-y+Kj^P^QZ20CyAjK)P1=)ngaD9e5P>FcVx1PLpa0 zc;Z=PaLgPJkHpPi;dDQ8eCc}Q!lVGA%9oz&qiOAjp+5^CFFpsDA%!j|oC+avnZMEU zch?+pJA*E*Qc(tsE7bG>Z%Mq}=?>#M1Q=F+2^U_S4zzSoE+zs$i*M3}O$8!31iwR>VwLI;Y*s#T;0twTt z+|qImr1qbalyuM*Er^RMG$!d^LEtReJu4dVx*%_^ld$b0TJVrpnj3mlej#5hDJ z2DY1nv!}82r&xo_Waj7x4wmoOkF19_Iv7qpX?oH)Qm!}Z1_h$~pw;iE(i!p&LAA$)~138jTDCKlU>; zBcZv)xfw}ATv`ihy$lX1)5pC%7Bxob$ERAsqna_R9@Lk)tPE30F4>0Rd7M)R ztrw}svW8?iY-2Cl3KZDoV{Hf3RLKJjC*_6TlJP9{@#dwaH#xzgh4AB66>WG=7lk5S zKz*qP9nP0o#W}ZCpSAVKx36fo800>8FGu~s)zX6jh{743(F#EOWPF#3l*c)FrTa=I zP$CkyjXCw!oTYUd(vwWG6C_c$72rPCRAOr|hsr;)*};m%GU*)DacF`&fxzQDjoUtu zt;se`q%|&o;J=S!3nz+)?4hoBsZjcdfdlSLgVNz=0+11hX+O%LvbRGSgCt0V+k4Ki zJ{-3z(O_(^*?FuGHLFc2x@Lt?u$8#4UUuK#`-iS{A=5MQHjsgsCoUdJl^(;r^z6yd zGSKL96@KsXV}B9Ao2?f?ml1(Rw2J)7-zw_1GC7fR=yMw4?A((mKPA}}+n?XMtc$FnMns%7*{s6v53Mf83=BWdumJc>iO z%GO^0=6+va;nj7Cfv>vVv0*|#_9DW4vjXjt=uL}AhvlE208N~hPkP=0tIkWcOdaC9 zwPdvj38hq_deooQPR;CgfOl_uw(HhOH~zD1ZkmDzu{Tr@Bd_6>xL6Nn=9{U&gx;iZ z124_nEnWx0*UAok5rKtF_=qx_bh_|C^@K7=CY`SUS9R!nf(9MH+`g6^F zoxl6F#9qPv+QLkmf2Y@GY;kcmlEZA`jMc=ZiJr(!s^#?la(zRpei@Ih_U|Md?X{L_ zxTPOJS5#h2W2pUlQReNwk^|xOMTy2@W8IKX*D9<*(UtmrlH&)|WzToBcSWZQ(^eip z`Xu(+sjbn+=0gst=Vxb+q<%`@hozl;F2m__1r%ky1x#xZ`#%pPt^%jMb0)aq0u!I- zBx;jc>X+ddsH_pdt=adC4qq6N>K(=tTpE~OU}_&vlP73k4_`15d>sGmBArBVFJTZ< zJ;=QL|7EQEzpNy2U~3h4yfG0=b}TmfDgz8M@^yIRJCmEKxxa{AE%$e`Cd*ATZV$T! zoHYD*N?+{jEYlzZ(XtY9O`mI822fl{)TQlKMH-Dq5Z}4DxYnvU-DMW-t%_AsCx&04 zW-`YFBfPF`-61^~awo@lP+@NJ$Q1mVwbIN|hQuf47b`CDR_UbrN&&53TvG9E0PEl|yOwe!>lPIGQaUR|* z<{{OWG_S~BJ%{c`s)d*u%JAqN1=9RXU?T@{L!F35j4S`+c&pqbd9O;&A0(Rq4IqF7 zXxje*H_x?M$?$5&vkhvxgSQw$w=oQ}&TXl8P7%+y6QTvosr9V(% zKUd;a#&xr--H|lp{4hBclHqc_b&ot|Eq7nF7R+g}kGG)UuB6%*fGs@gMQRU7M80;> zGR`!&Z1%8NatOWDO%QliOq;A}S~{2A2tVxQ!+2|A$meaeA zyUKQ1;n7-s9C^+C=N00V@nRbq->#3Th{r;XR9;0!{|PqlSF^<9g|F-g)$<3Rq`d%@ zxw~oSj*yT_mAcDS!YD~>MdD>SHb=rZHUx)2_r6Bj@_Dpvw6=xz$$(s6XRSAf&{F#% zJCOcyc6x4ffSbU0S|ROOmsJRJCb^deITN>>yojotzN;zPl@1w)VBUGx3Q}&Zm|Ajz|X($m)=A(~Uy^uR10X?5xnVl+N- zef-n-g#Z2G$Dtk;JSq~DEUx6z_3kLbtNfEO!;J*D`IFt!8)0V`i2ce24`IH{V)mrA zGB$5;y{>?GdX9(4K1fss0TKy)I{ZA`_Nonwxok0^)f*7-1cAvqb`Rt(TV_{p@WYwE_@jS$_L zuIg;;f3Kdaq6WQ8YFIHI+m^!T*5S{IL**@0be+d^cE&3HrMCTdx!{n)hyBQ6-#R_t zx=h*G@Jq1UXTC%PZY=!jQk9PgH*EnsPl;U|U#?fMkFjshC#$q^s-Bp{1$BoBQmK#3 z#!qgoe1-Rm7wvXB%(;g64^(1xDsv}o4mJ;oBo?heDHUH;Q=HsS`ekDCcj53(+t4c`s zNpsdPY>Dk#UL4MTiwkVGa*!1dCBt*H$Aq}%A?-v{mKR2gV;tfg-PD5<7V(G0ao~MP z7<|%C4vj5I*gKYh+EAv!J!p;))GX(@II^ZP0FE~HjdKOG7d(D^F0({~vlKal&Mi71UxKCJg*gp}TG9QJ_h1F&e*TRzC{e%C#DV5_piua7WN@_O)eX}OG-JTbaTQn5@>C!U%A5vt z!a~fB`UaOb*OpFhmzW0^T$u3Mc+>8qpxDEF7LEZx{j3B@gLZ5R%k+?`)LXb-AO&{-_LNSx+1p&hra=dQv{jf z4fLBc9ekmS$72!$m*sEUbLTT(U*kzohC1`_oD780ya*SYxkD*-@8LgSi>~JOe0CbxY=k zU6LeHKkr4y`8v>=&sz4ocQWDO_Vj6K1Ucr$WfDI)H&JxNyM5)nJ{E}w=zAx6^MtQ* z5`}A~S~kzmEn;08i#t>oWdS?pG<>I0Plcve$wvKh!?Q+%_6-|`FZYp~Pv1+)(aOMS zNGeXCh1fxAiI7H+sq~>%k|%6`^q$1aSwfHKhtGWdH5K&=#0)pIcQva^fPKIi(%JGS zlZ|8Pwe`aAuIds?XXeOww|oAbT9(;;b{e<3F)|^ z5wJfC7vs=BtQZ;h`t;mRM_1xA>!-8WiFnduC|q@9AjSSc3A*->$_JY$8@KkJN5Ul$ zoAbcO&Qd)|E%}ow>`Kv9e;oUf@6PH$ebt|_Jy`;rb+{^s*mFBlTPE94!Dxv7B1OrT zeta1V#h65=h#y8VB=>-Tld+#iku8sjajfuNieugSF*dv(dfHfCS~sa-A5|v*5UYDu znBFTUD=kTwm5L>{&zyN)J;u`M5>pRk-;r9l7I;>#+gn)47#BkT?sZ;#S`G2rx!KKo zf0Dwc8!rPDc-+9p2Sm(Ve2~G*W!dJlx@43jHN&dh5an#;u0Po#LguO0 zOOGo?=ynq2$v`LZ82&l6JKngVmfPW%Mj~)$iEGCvKeI0F>a18v%6p0_q$UWOLDhgfByoSStHw<5G8 zUQ5WObtk>;q^`r;n8dv48jPceWQA0S&tS+|GwI zD<-(&)&5ZBXq)UkOM+q#d|$roIZccGQ$M`A>ysX08oO9AgE+e_Q$lfN3ir%rN*y^S zeS4q&W#5pIjdua0?-)-%u~dJ+#0}q9WWJLUE5PZn?WN(>(PwX+Ff(jk!nsLa|M8%d z+N#Yj_MYu<5w6hn1Vf29nZqyZ<@#d1jzZdl>JB{rA6f~>`bpd#kQ&-PUJJ$CJfgRw z2mMgpa1IixYZz;Py7G%s&*3362eS9hRX#rcspj($7;#9sx)XhI`te|EW8mSGbN(C$ z9p9dUK9kyy&OY^x`hdo7fKkWzvBjR7B4YKGU%6(R@XAgY(Jb?a1`gv$FzF!Sr;go( z!O@=|M$J^^0RsCKis-tV<5Mo*643>z#w2ngyqhegyQ)ExiQO3vUK~-+CUZRepM%d4 zA~ViGB&JljVyqS*BuFJ(F2G$z%qO!4A+)V1lsB-j{`uAz(oLGwE&Xsz(|b%gPDpu! zhfy|wqxgQ{T6lt9Ds!Xj+9^?<88-9h-gbz}`pI>cs{Hxf%HP@8)1!I9RTtz)(8+Gx z10=#OndE*4{(=qmhR7pMnL!-S`?>=7g_#{wMzr#LI-)a;{vh3YB?|O_qO9bE>80;j ze4hu%LG=m8RDI4iUdrO@e06Xj!I}wzwCBY-C#sxQGu>Ir?(k)UdsjHjcAHQ%w>1m` zZ#NFM^R}+K zsVi2{bHI6b>5Qp#-chfsk|W(u&Y-uD=}sH(0B0)7E?Ul0s#ZC%CTSl;LLg(%T; z4WSNJv{(D29-C!fvvDf!pImmhdf{khEyy@W`5JAZo1?Qs5UD$p1lui9yqzelpIhx6 z8dD>DOXUYRF_6wXU$MZ;{KK}b7lT;41Jf6$s!FAfFTrgk#t&UC2lt@QC2!H10U=$< z#MLB6Z@@(Q=xz80_LAqbYME!7?26)t(Jq$Dob5+$a(6BaWh>qh=_cVa7rjN`+}^JJ zOajOh6K6+wB9`_YZ!22spM-{eZ&npy04Sbtc6Eq^5ik7aQ$!{aR)7+)G}=IdHyy$| zQ$&HKu>rn>z2M&%eB%MEjjo$)$(a9bYdrb#&S-E#dOz2_-RTuRY-RnT#ZE{0{r+$b zQ3pjfpHqWs|BwuaiWYp438DrH1TM;qp@?~SoZT#@!e3`BgV0?@BXa(vd7`{`5zwVDau+ChE{K8<;wI(%OT9@rT+w^{(x$4Q?0dy36IgKHwv_I^*g0I!hc zx;o?ga?`i)xlM{4jI8g7?5eRh!jb@*LKdi<0{(w=sA{79Pmys;7fa-HKJ30P5-hhe~iXh5|&2trHZi`I(wRI+{(d?M3{G#c988K-$MxADqv0xZF0GI@>ZG z`pOBh^uzGCoxJcquLBc?4sL)>^7pzVI7jN1FZdc)$xt$#c>e%c2Bm%40ui!|{*6^s zEntTnGz*uD+w7B_I)0Vw#VItVw33lD&iBWy!!l8QK7zx)+mFMnR0}X8t-5+C6FW%% zPB7K)L|VVR$9W1Ny|648lCsaEwN_Pk_^u;#JtGD6xzpoP9R=lV&Vrd`SFWFilkrTIiC%#WFQho4Pn=91St9^xaP9~;MniA7X^hqI$7FIQS3{wJ5IoE$0@So4!t;WS6*9)O_V z%AH|s7?r3T0Q_%;FN4=DU5nwcZNIgnn%LQ8OAo7`?BnLQuio|p!Lq|#h(IZaC}iL% zDPNXMHxSg=#D%iaaY3u&2~m_Em6c0=cq=>RO}PzM!)KCL@Tj4(&m?@3*oojT$^V|? zPg3hcj9lo>3KOJj1}GHXlo=ei%WNu0zoZnN2obh4NC6-!%{r*S~Yu z9J{~wq|!*=hU+%3%e+nGt?Kh&sMY;Z;7Ln!TcGnlo;F!OFW64`fXdlXS#YvVxz2{J z7gg7tIpCC}9o6R1(t4=J-Qyw_O3WN^LB2+eNn3j0NVk1rk|p*#vSz!V|?Wi!O}Uq3uXtU2%H>k8AA z8Spe+ORd0AU_$Hl1+B0Z+@kx9p|plRv`Gio)MgF_eu#gqGef8K?H-=}?<<5PSpt1S zTt1oL2TkClttdG|6|L4?_2~dA^kY?6QkL22SmM@S=sc z*C2Pq%D8X#%h?s~aXb5XAAjpfKM=tkSJY(GH?Dj~#BOm&yBbre1@Qf;HX{ zQvcr|G-}vq2$(6`tJgOyBF(*8b~(iinG>-P=CaB zxIP*FF$5*TB{$c^lR={h#6Qdd)m7H3MdI=5uq|5PpY#@3* zJ&dR}R(iowc8~DKJ~|JL0%mprLI0M+egEZha%?NUWklcLH?Tb^NND-}tu(*H@K%PN zPC_P?_0)R)X;MTnk_vX=hcQN87H+g?=REV`&pt`mA2UWtCT?>9$~zUvwc?hCuFkjU zhmfd=`R3tVfI1LC&TxS!@X+|)J>^n~Q=?_7+&&@AVz!4qaOJ9X08VxukP?Jk-U;#A zPC5w>YtVq={asD-lXzichJbLTNnZwabr7yi8Awg(ki?D zzi(Q1f3?fbkypjb79Ya~t{SJ0<})yz&*J7=P3t}N$lkH*Wqiu>6%u2U^=3Owz{aj~ zyxexAJ%sGQh?WK)q)6Oza4laTaHeA|iha$brk`eBFo&6N+c%|ly}3aFRU1ib?SC%T z<^>_mA4)NeslT%FkzJvAFpn^G2y9Hlc#hZ+Gn}MW5GjC~y#l@FTF%Vb^lY$Ft1tDD zg>f}IL*UmgqW-z#wb9$*bQF({ZQ?g4^}IXC`YBZ(+I6fi`2Hu#mu|ASqtg|S@ymub zLdeyx>C5o?W0+1%Om2cE-$5Cy0OnAC1<FYu^rHPEcT<{bDd z{FJ?1&qCId;PWErMK*u?_e|!0y#O0L zFP?%?{$WpDDHy!P1Wb>2>I0Q`KfEELxSV%09nXHG80|) zYWczA1Bo6Q_#T|)S+}4@lA2WeW|ZDfq>x3t7yf)pXE}3xq*K2BkwW5517m~5<+7$G z?{@~>4in$#6PUCG`5I+hSD+W-S^AFx4!$Kjk@hcDk#8krhJ^g%#B)7f7P1d3z89+G zX(;f1>dESUnUBAXRUS@wGhGqiogB>#*EQ$+77JN=MBYpYjS4kzMQ8tZe+8wLrA&K zXE}PK`7Sds_mvDsOPi*m3BJD;D9$g3A$OCbdGvmvN}e3UQ=VM&M76Hal3G8fCvggJ zK@0^Q2|Wo7W~4sfT|awmny2!Fm&qV5&ZWw6$AM8I{g({m6aQlXlYy_7dRc+3S{bU9 z|D`32)PRpQN0I;A;Y$V{u!GHe!4;6Ae3 zY==kT?hVW4LXi@=mN@EEsqUtOu(>2wN$0PhHi^zl)N*<^#2o218i-&XUy)xXvJ!A9 zI7egKIl1U!AsLnh zi}eCGl96=nk7eDRGw`%_vY2gjqL;C4(XvMgh;QcZ;_se)etfslB?)9aRxQe3AGN;K z6#P}^q>F(yD7PzS*noyC=J&OifOn@;1cXzgJINB;M(=H);15DIa+%thJSR@(njGb=wT!A}IZu_5o9=xAp2|NY+ zt|E;+0>;5MC%hq8x@mAVSgy4GNXXa*lLp@S2TMRlf;tm!oorv7^>q=3fPC4l)|kWf#QJ!2bCoOr$>Huc54CD+CDlJEu zx!XW40@9JIFdQHwRHG6eXBdMH%V_~m%b*9+5m-{?5;$8CrR-K=xmEd}r1K<}#evG~ zufbmJyQ!%r#M)7NCW<^A#55_-)qioyr8_r3P=eT#cgT|_pgG_CYs3e)zK_GzPS7rpzFJ4-^_uD;VT;DwGdc#bgK5z402fdEbDWD)gEcoIW4c9qZ}$gi2~{ zH3q7bvB}5K@E)#HD)PKZ?3J8bgetczaxU|-J5(AIBIku$`Lk9K^zTvc(CN*JD$m2eX-Ud%X7JDGwmqnFBiqmh4o-Iy-z|xHWTmzb9@e zvCk%n{pYkznL3=N9ti(XfJVw@T{yOZT^)4RzU$(JactQ^R`S ztCn^|+8(A=$#u=FNY9J>&-~nt0=8M#ewms7+^KZoV9&O^tQwK4(f?aE?M6h(==6bV zB{Uy}uL$b_xD43glDr>FYdT38UU#k2xXjU5Q7KmZ(|`R)@5)5UgrB7wm_IC1OfXWE zBwvWN-bWlsNBE6Z@NJ7RLmejswT;91ueHkG=ca-TUq`8ikmRA|i2K~*{&hDzOOU%e zgk8Jk969)x80;3gjZv=lsO7LXVpN(f6DrdlS`VYotpB&O9ZTgp9tMkH9o1o+j)BHpsO2wNi^brbu=LzTxv4HyMxQoZva zov)~W*T_rl^TDeU+hl6h)ahmGr(>nwH8&taXbFENd59`LVCBg%HsfeYzP%4dFGF;-^VV5T8p>OyWj2TwHPy7 z2V=5*t1AKDS?17a(MwoQMc)jT{T=J4R^5@m9CHD3&KPoX5pNf-e1m(fFe5o1Ffa{K zfAtZuNtSEu{on^c=pD4<3)Wy(lF-J}!W#PLy{D()y{k{mkg^z39zg zwArW@Qc`1neO(mAA5S( z0DjAI>XBr&`pNc7<7fbUOdi?4gnvQ~B2r!4e?3%0BM_*ltPV0y<3c8k8uXR?fckx{ z7oW4@8Yze>Az&kG4BQJN+2SmRvq>tH;D6p3N%^iMIo5|p;tPO2Zu9H~TQPmN$f z8#e*~0+xh6ar-j z4>ZqNQMU8*my_#_z*=at3 ziFOs|BU1P4xxz3yf+&mJNw*TyC9d>PXBHh)c}5fFB;U_&N4CT9I3Tv%&t9I_ zr9QUs1nEfNGyHY82uS_bj`iBN@PNl;;Ss?70OOdIaFQugthu*_&)9R!sd}cV!EXj- z&qZU2cz?3^UA*_rUW(*8T@HN$pmmSB?6Yq#8ZPr|V{tR9JCYmzeJIi)Gkj**v|$2c z1xx{*d(VzZJok-`CJVQm!Er;Jzr*q+JpyWa#RrCi$^mLOM?ymYUC*J(P z*BoCCvTtx&5ah(43p9pRTa$mQcE)9VoZV!Na*BAf%m=MivFPx_G&Qw||D2tcwD0)6 z0!-c%x8$pp1%0iIGaJhzhsVRKJjnJ6A5V~zY0!}T~||N z=qBWAMap4hBxts%*$Y!wE3UK?M$9bruQ@Am!uNsLM=d_ErFp$8R8i>ntVhl8zL$F- zLBe_q-mHmkyoSb^VES^bBL0?zJxLJMF~FkvAdtE!vdDi_cH`k28DTh0_pCS9;bjbJ zh(bp>-BKKB{Ay79LH>dLJ=i`7 zmqHI&ptT}{R#lZZ$wrzQ)PJqT-Va8uI-BJpfui~diSS8o>;x($sWs@yEG-=;2hlPruWmlUE{EUVv7LnO~ zB0u91)S&4ma9-Tcnt}aEhPlK3bzpqWj6+;wcu-Ivwfr zf}G#b02Vf4PmTF6^V!F{t^ko|Ux^FTDO5)xn`fo~|DTCoE=F*HIo-{QjdV*L0Yf{^lsLk&Clxg-x!T9%h^Q5tjUzEkTEnX=GMy#hbn77W6{8Kjim?a z#q`TZ+zb#c(be(JBaq~GnafhTgezvM@TA8GZE+-9wamT9xGq#fI%Cc%z}f_ zc+M_HWxZ6oOZy>~3Jp~f79Yk~P;v5H{Y!78x=bgZ9j-z$-A#K7H#(9&e5f?0$FTdo z!>3ifQ>6hP0s)vnl}-q!bhX1zj}Kqlg86okz~{$kSM``t2KM3;=U(1UZ+WbYunO5s zmkKzrm|;V$eHG6-ccte_-^CSU`_58-n6)R-W+&63jz&B&CD7VsLSkJfWdOD27jQN? zWcvU~ZrBt{*xC1C5cqBU$p@L_cFA&e(hn{))Y(K8x@rp%m%(k&#-W2NOs7sIrDHPa z@i~I#0i3hf$k|yiY6{f?|I*n#IX}Lwz9G#gYzgdOq1LQFJfrQNt9wc2jSCZ|!F63+ zY=j=K4MX{5?jMmxd_aWI`8k9XowR@5g^J^8MwoL%mfJ{W1(W#vy7e`1XOm{J9R3oj z6?yxtFS|D`s<{Ib;orG97pERbn#3iC#FzCN1+3B zew0ZP&|CSxHbi57W8D9KmoHx+Fbg}3_*5aGc!b0Z?T=D+9R;!{uST8TW*|EtE-_J> zYBA35P>kP-ZS6~6{t5Zs^x&63*q~uUO^z`o7*_DPuqd*ZwPmj;w42>R^z}Me%{|Rm z!;R^Y-BPi9ja@-iW41hRuH4(#Q+Ss30fbIuu{E~D&5Bw!vg~9}(+n85k%y%2d4r$no?%uk(X5XAB)n)r)GDfAb2vweTPM~{;j1#?();9k^)HCnY2l#o# z{~AK=L=mqA_Nh=0q!z`be`%qnsvD>&J}^$eltjRPvPP8#h$x{TFrikIBr^&^ELv3- zErvg{ExvYL?w2rZ55P#7y}P98Omct>I(?Lm$Dlk)wY%?(8C>f)&q;yuz1|1HI&Uyj zRx?@tTt`OinPaW_;zNJl)g$DC4G1=*`kZLA;qE{A)u2^rc+cex%}$Ely@jM%A+~t_ zZK#v%YT~q}xMR!HcY!0pMtznHgiOXTG0@x|6WGX*#hy68gIBztUBAtXVVw1L= zg^xdbTgnQAL|TsMxgO+1?~vS5LLX!HE19}Agy1b=-14J0h}66kl8k)>@Xi!Yamv$e zF_W`as!Ewot%RbVmaMHbSLbbK?W(mM@{%jqs*22~Z-Jc%){K=6B{)$b#6$N~`0DTV zPy%Dq!ld^Bsqp5cMzTVjued;!)w2OsJQLTWqv#NO8zcXu!$o}%6XB#061TvKeHk1VI}>=IOiNJ*9MU`@d%=<7Q_YQfks0Su7UxzBgK;w$rj z=Vrj8sW(t}qymI+oB5$x|0yzfx>hsOfH3v@kO~t=##V={OnqI+kw_nxpC-x1N2-8b zab3)F{u{9(%+Dp~{FeY|<^Ayvgj1>gV%AI4>8W$h>81n;&eH{%ml#i9(rxsT(d*IC zgJ+}c^_F}$dTmKL%fX#zT@Pk8ma?cZSQC{BXhK}gg7W6jhs&2_PW6`f9=)hsiZHXL zV!XzO)Hf4zNS z_wj%x_f|dNLF(R&+$^Y3a>G%`tBMSxv3xcT;Id0{J3gt)ggj!ni!SE`A_3VlR}pqR z>LXvh0(M|Pt}O*Sal6=bg3Glk!#=At9+VZicv3! z1Pk^fOp6QuE`)%xh;LLxf=IPx_N=&85_MeacYmWYVhR*GyH=Z|A ztte??%VD#TkClUOj7_bgb}w|J9Ue`mM$+u>=f>J9au*~9ioA0N4XUH_R%MXrJc0N$U*gOxw(ztv6Cp)0!i#?&;=aM{jemM;* zG&xliZOqS?@!_pJtj`0Dez>w#rjt6o6U{9#!-yW))Yr+tJ9Z8(+U0o_+9sC-WP&6o zu6J3e1R;mGlxs0*ntx4H3;9Z^ENeFHqdfLPXFdzE=K3#O#daFfQAPV95^Io31}^uX zU?8>*gQZ&a4Ih8~8k$=grIu9w^Fl@Z`Z)R>>)qGaFKawz(%@^ckbc2PY!l9uOxgTP zWOXVH)WhV}1x2UOdIw~suY7?R&Moixw!^!_lRqNNBFTAanv#`JR0b`aawz}OLJ1kX zjFO+^rh2i>TGheXP-HhKjo}$8V63Kr*#|o!LN^B`Nr($DmORZ%T6gAXh|~kn{*+d^ zXvFc5T>p;1+7D;($7Eu-X6Gls7^R@(B^N_GSj?(j>3Te&qq>VV2AlG}XssXIAewiJ5& zX-{A;D%%44jLpdVMm?D$TOAcwD%;v4O2mwRYd_Bo2g0G3D=Yjhy|X zFB<$VMjH~-XZD02n$bNro|=RfJw=c2f7v_#0+qtRuc{p)R?q!EBLt(XM_Y! zI^c|G&nLJg>pfa=x`eGRlumIV_RS(@hGGQ3cD3-Xm9;2&^7lVbvkpP?ZmsS)2(WSK z=!?ykeMhycAMYGHHS&U|?b`^M&CPrhPn>31v!NncSnI}nf6ZUEv2I6t*k|jiOF`R7 zSWr;(Lm_%IMBBsd_!X~r-6XgZIDA;XY#MVd2fm|x`))RvKd`PALxqmT;+&oxw#x#t z^nm*eJxPf!)Gt~d?C_iH>>2m@-?v!nsI|_JQt&B%0|;djAfzYa-o0Iy!y0m zd{13@7HF-=p^vxc3{le0=-HDkGmP?Ct|p|J8S4c-tB`j`$!`O5cmRvUGb~zr4qMG2kFy~DNIc+2oqJ$yMwZUNRFF)~J!KxUD8~y`T?U1w-jw1h6}j?j`y;Kv=Ugj%nN=VxivR6-iL#A5u-JMA zw{s_^@yyOP`+Rq?5>@q81Z*Mnvy77=Uh<-z>6ZA1zp#p{V3>mkEDV&zH~*9XzU2g2 zYzK+OcL(EtW3`53tCgK9T3E;zKgIIKzHi&vmwSH-g1&$Wd^yL}lKR618g@4cr%We~ z4;v*Lm1aZ-e)GeU3OF4982C*46J>U^pjYkh%D(_n;aqF=3-H1jX#uDfqCqIdU>ecP;=S7z{U1Y7a%aQJ zm#)+xBZ6Pky%pB4dJ7~mM0!M;d}2GUs?9M+umg{WxPKW(lK{v9^N^{zVVd$erH(q^ zpgzjJaG_So#s-o?ZF7!D`>FKZK;F~T%>w!-kIgdJhNXIoWn$XNW85nyrQ&LM7QG9* zLjR!+V7PVuAScHXm4TBXcrj~;0@NUX2RM}WGb)t7<4kuJFGkxQSMH2syaU(>T8+&A z`p4_)2?@uAPD7rvT#31W6>H3>K4Bg9UB%H590>IF4W$xCm^q*2xd**aK5!;a~2O6d;%7p%ew$e2gWH!%y zh~YK+2D`z2j0Ys?31!97ps(n{f$p8(QQu|SY)WZw#&UEc9bGLE;?q$xHRSr5Yz-M= zR}T4vU7l=W;5M(Neq z`*TYK=N7uIn7;AT>x@%I?zCqe-m~6%hw(c~^x8LdMrC{3X-U~%yyB9FD-&{jGRpFb zYUABnVrMI`y6QH3{H+3;#|+;l&^cFj!n`Y4_X}ozc@J zX>sy8OVmb?J6&+CMlGB8XBW`w%2K3bxC8<$+Cv92x*=(#X&|(`@RtMMVY8IG_PJw^ zS4UVhc)3_A20$i7Sm^ayMKs{qY1RPHWO5$p4*@hh7BxYC@wtDZMP914&|gxbJ}~}U z{PRC$TmS5FAP-WvDFJ$^N>nkiO3<3PC|~&*QTL7|YhxH%G%df4g-#; z5B{FP{OAAj z>mClNz8%ZT9K8H6VKv)F2Or$PMK0l-`BNw}x3Yz`pSi+(g)p8WPlIV15r*UJ8}e=5~X9G0We)ojPucz1)k5@)NnNrpiv_c(vf6NO7@bioS;bB5Eg8 zDZ`^idKQ+&f1pI42|nP;M?W6jkq6SEh-MiU;R3%O_#Lw*RJ~xboic|gb1j>{wz#r3~9B|mT&@hUv z(24QZx_!NSV}Y|56xTpf$r_N=HcizlA z+bK#u@L>%jsz3fVU`@EU8muoQpMbe+k|Y!B^9gO8>OrWIFyM`W*b^BE&CsifcGZfFJMr*oIIE5MWWTH&`CXJ=p4PX z+jxR+6fVu8qmJ2ogDX;clc8yR{8#vvvpu#w0{kf+Ji^bxRR0T_V`y= zgX9zLPC-}8v3bljEN;Hq+ebOD(T|#XCZ^8nx#1@nqA2!`qi_ToRF+-3=+|x09SveZ z3KEIf7=BE)n;E`XO{$Lq$t!2q;6U0>_|AEN3%+cByH5=!THKl9w)m`FL3QEg(N%Va z{Wu1*Qj;1*_Lh`+ThLw(u-u33L^xx$k#(8CMs)&*G9n-Go zRgGu8chHEj0quW{3tu-!XZ(}x@9D+$s1eZq4Cv^8s-!nBLLT`oScqD_qT*Sf%BeTl zq9%UpruU=yLEYl5_f*o&Y{i>K!MWM9uw$piMijM$=u5c28Gk8XoSm*++d+SVDW|@X>T;Dj?9U z$M>P$AlXg&CB$>wajx7_DYvD&D77#lUIb__N;zHXCCbiO%_WQR@Cpd|lWN}2)_|Qg z4X)uX=-H)Cub9voc36+&1B-2$s3+M~GeDS3{iZpP11C}dgh}EWt-c(u*5L~ zCi4FURP22*0|&t8cLju_S7Gu&Kf-}T76QXgYXM(yyRQp7ETTpB)PP)u;#`X&=N~KT zuo<8(+iCnHqZbk_zb*BUwJ!=^RLT#ui#ED2j zVJBxXXcA2R)7)8Xyl@>x7p&Wh;=F>6qk)@7jfwtFhdPiV(N)ten4Vt1AwOUDhpU^4 zM~<;Fi%2c~p-g9ohr{(%UO5q&5jS4;;Yz2xU2g5xVS@cEVgMW87v5vjkxZ>iOzeK?*kXmPD zk79F7A8@N-iQnzl)JIR0;RS8q)rqf-E4$OZAyshQs!*8`+&xLH{TU9JCWz3tVb3=V zGaYUaM|Ji4s&yV)R_Ji!c;E_9UU8k=Ck)d%rzSD_+*{TwJjKW^gHNH+A3hFVJgm&+ z`QmAn()G{5O&h3rHm6?7EdMDkVW-q8X z984q5`+Y2rF8w}>cUq6qpF`?t&B?}Cngo@#@;kXGTir!tGJ#%BsB2mHWbXm^B$63&lb8v4!X z2b*a3sBj+!#d%O?LpyrX0leAR83UWM@<4^|jAD7f?NFe{Ww|y5JXGD62Ghx=*}yZV zWLgP4oKV@>uydiZZSQE^!93@@$Lxe4>GP4Kr#}X1xbzmVKyaN^Ydgt_sky?FIR2ll;%?`uWGR#&dj;@cJUSB?$D%D zfZ~a5&tDyi=#R;&Gd$2NEzmmGLqs?FX|DMz!{Ah$ddbr3xGsyS*DJ&6kfE+nMMZoW z&rBO&iartaSJ^JegiDbl#N(be(=26(uYnzH83N6@+B~C;tAYh%t&|~pjyk90H~OG% zpTI!GA!5!J=E^KQx=YPNZRawDTYheW>zyACjoK33Ezbvy2SCA>u9i}40s^&+Xuo!% zIgO9}|DM<72(qyHO;rDDj{h^(0|nxQ+#}#(9MJRUK;RXa3!J@&a0IInDSv2C+w{d=4IUF^ddZTye?$XDaVbx)k@n5l(#qN2&*{$`7-T${jdP z*Ia!g53*_5)(O?z4<9~vT5k2m^wIq3O$8JfR34ElHloN?A3RF_W}Q;j8-#eKt&cY; z(e%hEOXHM2-hZu%$=adSq>0?ns5{y~*t=gpNouN`t2juGh3m}f4mEv#EfEUDRf3UA zD1e6XF=@vmUZF2S*%v)>84BD>z6yzTad#To6W173a-KAy>gTZM0@il52Sf_#S1r^^ zovtL&Si$@pjlrQIKxrs`o`kAukyofoizY5=5{+YYvcE44LAU=fd%vciA|wIemx%rC z<~pP@7>Z+x^78oh=YB%1rAPTY!r# z-Wp3Lq_WLkciB(GTW_vjgY0vS4qJ(CHMCRPF;4x-NdQlIPW`oL zyVmUQ29VhIIf?A-c-P!iXv{A1XQP1o#7_qZt6!_{VX~{7ZXqWqYy|6OFlAu8MW68n zkF^PX|L?(Td+;R8!J7r<99Di~mU8;t?L0sMuRA#-~9ysAt6Jt61l0 z<)_h~sI74ZnxUYS5$+vCRZ(S2UCD+w=D#t463mfD>5)hlPEYB?)Cg}93$jiW)<-@4 zGEX+aXkK6$dIXP~%`YGO${Fk3gebG|*=*FS%e;yLD_Ir5yqhTBZ!0DTpV^Ii!z8Xm z15Zktz2p_n*gqp{ez~bucF*(-2a7`AAP3ffJN3gsY_NfH+PYi(03~5&5eLb=y^CAaO(Gp!6Ww0jl#$s51_1Ta#ayf2=}*10Zuk zK{zp~lN0sD1R24DRAvvBO?X!*D(u-n)Uj4hJ~Kb3qty%0o`mZ{?BPQBDx?B)!g}Jg zrsr2t9n&vonOy*7YIOuTYt9)d<;a5g4D4NX0bbn5`mCgRrY(FGB93F~n=jAWy4&c; z<5IA@p?+67!yy^gkk+tkA=+(Txl;d!oYO$}>RM{y*&*BL+=y#ujEbUkQM=rCnGVt% z71XqTTb2!KEi&}9T^!wU+isI|(#n=t*alv)9VQ`gli2ZCNSi`3=|sQqkW|9SbJObo`y`yheSt<>{x3qH#>dT`hA%_d zYKCr{pe0aXb}Kelw+EY!|D@*8>gNCTfTke1SaN(==@~k45++N>ad5?U>lnL9gkpZ3#TMw-awH1ZtRz zOl9A3ijDe|9V!y^-XG>e$4Jn?prD)H<|em+S=z%~)ehdKoBBt@ODYT_s+a6o}k=jtNb5UGiw_j@Kfdif<3>V`W;! znecZkt7jcP$|A(YQgyzZRV9XA&^y{=a(!!xpqUZzh+Xac!=*y~4K_lA1Z)^@X?T~#R)wD8lGzkh_9Tn- z>_DTPajYap0n3o0E-{dJ%LxL?)9}-Dn2UTJx`?~qiBKE%3) zYSF_%W|HO>FTc(5L?hh{{uxaE(kmptN&?RJeXY^t-Mk%?L3>s^xfcmGp%fHy#GiF1 zoILitDbtGz^p(N1Na@73P0&*OJpuAaiNd2G4^Pp#PaCjQ?ot{Ej*l5nKIdbZXuPG)z_>#Y) zC1M#$9qCbVRc@(7dCwF2A@>BDK7!Jd_TB)NPkM0n{ zZf;H|zvN>V?%x^L%tyyz$$b?5?OE#&LPf1Hvs>SOgNr3PU{?H8$Y~iX(p%xdYQww4 zCWtU3%4<=o=XLZd4m-B|!|(k{ zdqV8C7_{|Fm_PkK*GITaE+@S))nw7%!Ri_a@psipUDp~Xu264oxb^FBK5v~zoow%x zbb-EM;wegp(~dQ45m0a#WZUVgNQNnVKLUyAo6r#C$0db<9PYBs-U2QabZZjKHJ>cev2+{En`+LZ>gAu>xUBFl6nEjmkG$fn{RwZG<`+ml68A{3h3@09xn^&-13#Iy`Vmj{uTiI}l2^AaJ`o zf3+Og%Xi#6ST?_*;$Pc*o$CR%8Qc3YEvRi!oEGRGKN~&Z@rP>gEH!k`M3jIOwP$n)tMqg(>59LKA1HSd-$#8vUcs+w5u;~%1|}>_Rcfa1er=%)^snE=O+ub54erK zoAY}Sdgm7@u{nI9ezR;4-0hl_&$?XNOwrf!?qJ|kGb0lb$A}`_@I!0#2mqa`M2vbr z7djwmoZj&9X9Y+A=1uW&?Utbl>6!8cl(xHOc5BZGOb#mJ-ZrAAE8FSv4C^FmCgnYQ zgqz`Q{@48Z>m!h0)sOU6Q1W|QBLScoqr#cO=|=!t&W&kPd5Bv>xz z2J~?8%YC6t&<5r9i>|VIl*x_4!`B$J$6F>9I}B-{G4Ut3a+^9`YaC{#clR39NJziY z+Y~q<{&tX#6uBT{_S^74dA$lpqfF1x{HlAR6cf+~{<60l{>Jv`Yg*+d5XP@q4q^$Q zA1j8T7f>L+F*HPuCd z{8!3KWK$p4cWSad%GeD=E0n~Fh#W_7G5I|(5e{@8KE_2kVFz3b;%A#>5(_9SnfY91 zcrOv{zHPde6}@xb{gLz~|CJ)GTGkQ7ImbaW3wG<6&X)9|l;4*Q`%^_O}D#cSxNx8B` z@k3IeKTr6u+qRsFv01b>I0JWl&ySSCGj3(yK%HSYxh2;#@C^YQt1eNmVke7ncJkFH zx~)wYlV{&Y$U^Zy2takf9HIp_ST~;)e0{pL>D$Cx$?Cp-SEnk-Pp>oXV_d}J+fa}d zL0vanVeHv%Kz!+D@D&>L1Qt~U`By+e+H)$NIm#a~1~Q6OHyp$sXSJ7jg0E^bzk)8|Nprllz974H@{2{yp9L(es10TU1UrU_iU_q5sG7$fv=W z^}jhP^8dU@C=kBUYe`CUNhfYfu9P?EYaeeFv<*n$nWQ25WF_yuzNL(^b8yd2y9ypa zDy_6g(IRifi*NrmfVg|))pk?P6|KpzfJcQ8Gc8j;AHFly|$|$&sk|b^+?PK`t02{2R4>pWVZ2$_Xu!g9We? z66oR6vOaGO?JGo!J{Cp#3+%=>8ri=LQrby`g(+B7*(_6Hhv#6v&oj8ucGZ_tI!V8b zJA1wFmOJgs#7VD=xo0B6r-Qw~l-^jk42tdq7u>4<9^>8Bk~URq{I8=PallUIM!YH?7w~2gq1wOfZ>N7SmyR%8*bY^sy}v)x zlzgbtV;6sy_)$89`d3H;?Zt8zV~gk*=qs#YaW)u&nax*1KHgNK)=GJ7s2Fs)*=h=w z`zem>yxqIIl(DG-u9OJO$uRjfME!fT@ljS}yi)iMM1F|3N<@9=mN}r33NxyGLfIP! zr52l#OZWEp$ik&{L!WqOi~sayelaZRWSXG=v-0ewXMO0-?h42SMlxoy8$6u3(?wye zu}~W8h5=>2d=9pgK3&Qmgee62`sxCQ{$ee5SkKG0|#Vxz3h*p?LDgrrD9TvVDz9ki>F$ znBISWTk2P)bt}(`4fErK&%e5A06?>J=ljG zd6USsCW)@S)8w|UoLuz_vV-NA4uA48&+~E4BsH5iyQ<{q2p3%YUmK%>O- z=xgiWFklq%lXz-KjNHz=-Y>+`*fv4tUz-D8CvcmPaM&}zJgFgJu60`UJji&oml5}2 zXL!r%i@D%Hp&OBAmO~tUyk&t5#Llzxs5N{N(uOf@Ou=qP0b0BhXiye#nN4$%$-S;O z%sbWFI(b8@PRqET>RyqGkKDQCwY2HDtEhC-z5@1j4DWwMD|qFQ5pHiTz zarIcpdkoWs^f^!W-gT|@IoU1}5Kka{fn{c#$DenfH#o9G7KHzT;$n0y>J>R8C{hs!-%;i=BhOL@0ye^N`5e(P^K9$&50p zY+gF@Oki~s$H~=~m>BUT7vxm5%bv2BN|%?hc_rDpzzA)n8k+MS#d=G={9l0cN5hCg zOg~-z{F2LZXb=fGdG`!9p9poYJ#h3EP~b+(H-2J^E2_H_Z9r7FE2Yup;mW19PA2u! zaN~8<>Dx#67g*q2<9P69&2Zm5j_rqhm93E=V5VB-7@yN_NN1jKK)w|S!|e0)k=5uX z9hFfF!hvGda!ZQJSg3`lGlnr@XU{b!soc;u%5nbUrE5c@Il`kx4QKq>mESTwkjvLP z*{nIVwUj`p=(!X%&4REtBfEu!xU#6+#9OR#<`48XT4Gw``UKk)KbMKWZxz-cQ&KX+ z#xAfWU0Y-bKNCIRj?ckn6IAC$AgnQLUEan}|=6m)Ee`2~z z^f4b?)|T7SmwTEZVcw68kk7nJiT8{f&7sLPU1i;*<_UZak%N*X{I|;spu;=(p?eg< zb%qnCl?Djc`hZJ%>dJ62U;q)~$4)Ur_ggLPD@%jj<09L7SQSzbDNXmJ4$jDVz6cA3 zykLwfO&lWgBxuzRdhRzHaZ)0P@OPQsO#4?*(%Rgz%5<qf^pE-V<|L$WZ8eHk1#X&`uBgC*sSt6z6cF z(?V$5G%Q0{{YaiMSJ&5=-Nm=Tw>ivdIj6BdevQKFeex`HblDop-H%n*=0Wbeyg;R; zaqN4HJc4?9;sbz=pv*IB?panpRCJN}6g%QX(dcJI;%PlMiet*LZk|J|v?dl6WQ3 zdP5jG38Z7c98CxcrV( zVsfI(rp-nRjB4SN8(%)!n|Q+Ig)ctuHL559a>>JG1gG%gu}Bsc`qB92n#A9PqGZ6^N{P#Y%y7CHu4j*~-RlVN)w+Z$GB1#v`Lec8C zHi4rA`y2P0RNxA?D8XL}(CtZ9Hwp9v9coo=MzJ7YVn9ElkBs1?$f_X}idBgHiwJgW zM1)6_!F{8YPrJ^JCQ zV4Z`|RKD;qFs_Bi3hpon^(PZ>KE+$0DNoVmGXM0;Xib|Pju!asPU1rVVu1@RoXQo> zN$Tz8*eGW8s1X=HYix$?+>CeEpHfvfVb871=7Sjd8RmD7FUvTOL>+&J9?2%xHicDb zm)UZ2vE<*k=OF_4|hdS4Om)H!=C47Q+Hrx9JHTRMA`oE}zDO0Dz-c47#Opnjzf ztTN@W*^2fxbXd}u&#mq^&l_z~q$wG7VFk&sy+~E!7P!|*>pfPCbW|VinbF%qu)n4UThiX z0PB6;m(uExU(}I;!!}`t;l>SgPV$PtL-T_}_ub;{pzt;6n!qu}H~R3)8}ug%cTw=q z1usj-w?+B&PJON=o#Z)=-)jH!RSdF62W|(~EiI^-XDl@IPATNH@<@n#-N&;P=d1a1 zWzqB-R7fDTUjE`9(Iv!GQ!ea%+^@L&;8=y$)%@nos=_4A087)2ysSV}{MTr(MJ zhS3n1Bur7NmQO;oI4Au`?j`zJ!$Xl`hhLj9gDh{WF~4!dOaE3<4bTMb$(E}=6zkXD zl3X)u>05;T`f3w$7OX7kL`7+c)vB z^u57;dq_fc@wUIxL8*>s+@KA@SkKALQX=v;@#j^wOg}C0GfdZL2QtQ+ zk69m}ihH`b<)pYO%9(Il;mS~>^;rN_5zABCXgyviM^3q~C*csG2ky08HhvBlEk%F0 z)OuyON*>;en4gyqdfs znKM<-*MPwqMBd6noOS!OSi@>=TOQgWQZln|IU$}5OemKTp=Sb`G#^baG|Ze*!ahC* zVHR*r$3KhK{9lEn{y(!x6nIqlXfMo$&;K&L{Ld0b7G#f5U$!0OWCKRSY`1^4mrCMpi4Gd&OS^|hxh&w5zlG*A|BgaP2{k`H@7U*@Xr|^+$28b&W6DI*WEKCegXDLLMW=&Tam%b zPyi#R?0Q^MM{)ci_~eD%u1;Dj@P0jRsp|dF{hAD@WozQLey}gQnK;c45SV;Wr)$9? zxsGyb6S_u5ibE^$n}>Rl47tDKRW)9h9Gv}50=}sz$ttv{Bftbmx4p|UxQgvN%Yo@& zGlXkk)az9Ed%$?Ww z#6YL7Ky4@>N2EN86`*24G0G1UU{<212X)(A!H8BR;b$he-`i`HVEDT{;I$}i0zVXr z$F%f-hk<&TLCFZo;TLt&>Xs+kQ78gl5$q&e3A!PS?=v|XM0e_k^h|}{w}~%K>kq7= zRnX{gP$t^sH{q7)CyJXUS;{PsqjBd@wD+({h-4Xh)AbcwJ_auA38kARS}PwZv4mdN zK@6KMN*NU;ZvslT#hf#(h4Th=vc!OZW|D+=H|!XGf{xfC+5Jxi+_vt=2g7HCw-EOy zKMUyT?KPSKU(CSF6GjBrkp}t3H3Rs&)L zpYFx?c9-Nwd}ZjyZWw0ts+jR1gU&2l4PP|`zR<{bvu?gjgIt%UI;xgBgr8ylu#Vz^ftYZ z*J2mWnX<%YLP&*mn0R$QEH?molzi>>K-9jjljcl8s>KpcN^dh6-7)xDrV;+eXD?=L zkoK*eB4?UDYa1d$z%SU?*KHB{GYWUXl)RO=+m8~%Aq2V|1dOql$gHjKpsa3a?H16O zU#3xZ*7oAM9{^u|poX+_l4`lM^_7)$J1;<63(*lD$Wzqp>#Wd%L|DZo)3#&Gu<2w` zVE!S-0{oz}EB9$jkCgSI6VS5cKk3r0>M*7_XH(GZ@1+|C!tc+Qr9j7y6Cj}KdZ1HvuXF$e9>B2Cp z@8tc*10Ca~lKH3O)s?--eyY?m;(w-|I(sYAJ@x}-QWFD1h0AJOuaKeyk+4le??$u>>{{b0ZjO_xtsNXUL$KD6yEmJZnsb+pI zqb(rSsGH`!%1eh4x!9C{t$9JuTrZUoBKCb-w@)tQoj9jbDr3)rDgU`-pMImq^X>%H z<0L%&e@5^B>yBN0#)58OAs`eB^mXwLCW2ZE-CYTFn(zL`)J~E}IZNId@I5~?Z)^_M ztFP|pHn3N70}4ar5Th;6-b(JuTIu~~GF$JnU5NS{5-7hdL-EWsHf%q$_YLLSe3L?_ z=@)PM8gk2UL011nB*rO~K;FA_MbX5JU1I5>Z!p;6T4yA-U~QXl0)45P0#K(mqRqo1R{7?dZH>&) zOZ7MNGjX6b{D7R-@oAD-&Rc;dM=4ESh8>PI5B7;N1FvG~k%pB&G$@FaUUd;rw$<7XY^M z@>_Au+pg8qgSIOQMwwna06bb{Ynlf0*k`P;6xSSlT_srgtm#9oHm8uk_jQNJID&_j zf)1*r;-xtOB|wc2GT4D8?=*C*aEW_GcihT5Wvjx@hhOb>bEH9u6ZvJ~TYU(^zMS@Q zP$qE^7YhvQz{@HGC=qA?w^j^KaX>OcAv+qBF|y9y!@B6RA=`hV6gEog9UE(ULfFHD z|B2B40NGux-6hBn7qsPyd2Kizv^SNa{HO#@Wv8&U#EOwgVOz9cm01wR39mqj`9en5 zN9Dz_&!;w&Zxa?9X%+n3jX1*gWT8I!flXv23WY3^*6{?7pexU*q^$58(gLEKh(L%jE5 zm$l7`uv??AUQsOXFz|*N=tp`x_Dd!V5h-8dm-A!P%i20Ovf?OV|CL@EX5%aO!*I~! z@A?+~ek}_6^14<3a*JI$wWxU@4+s2JUH$@tWX=1A)>0I8HE)FNj*wXyYsLU$U7*B1 zgQ18Y6P+Y(=SiQ8RY-E3xZz4KZp^c#;C!;hv{qA+hO*Y`gHN*dJ*p1_@rWvP7C`AWC3AWtjx$W)ixT^r8xEBOuKnjggi zqy0YD#RXUHtx|HJrX}Zf z7H6>>K};%_O_S7qj{q%GwZeta?Esu<1xlq4OJL`7e$Cvq@NZrh^j5Ds{1-icQBT$Z z8LeubO1#MQS+~?f4iEcPXi>?}1JAI*Pbo%%iL;6GB20+`hnTvmWwTMMHa+L&KmA=X~Wv#Bf64agCMFJmhDe&dZsRsZ^l8b^X zZQs5gjD5eg?D_}P(xytdfGr#V zG6`Q3Kb?%CD1~!f(H&^ll5#foiYXEDfognt_DlWQnDkRJxDb%_Qgm>CR!^Tim#?GB z>!}lSk~8Da-3=sQ(MjT(wC}677x&+5e*C6gI(ER|G+SRG8H}aH4DZ5Ko38Dy8t`pB zTv-kFRh6+>74>y)R0Y!b?ND6V^-Lxwoj7&K67zRdsBUv-ZZNb}}OtbjSPM6gY zm?#oS{}QT;IK^mmx0-^FUxh@`g!6zAothUSPF*(HvI*>B17{|mc>NSfKSD8iWu5Ke zj7#b%7X?q4w6B5!a9^LvJWOu%V0H@09bNgq+eLDZ^Uw4^O_uZ^+H5Nh&bIR{g5Sf+2ey4mB|l=e_6sIc>#))tC_M?Hb9(Fnws3bl(4(mDG?u zk$8HjVNQMbE_h5jP9c`fV0|6rNa`!d&aJwM{>~o(0eCfX9SctSi%6@V48CQo)A~Ec zD#F6thLxm}ju>+iVEOQHR3jrO!k+mieKYNQtJiYLC;A8iMPbr?{?J#}8wf19fhBnF zLzYKZ8%jDh{{p=DJ|iR_@(z*`;g1&cYeDC=peh*uP|(>Ds7j}S{qy;#IN3$0-WhIP z78<#67USq*lN7+#1mJvVQJKSza1_eFzTBVS@$RcfTfJZLfEx>dcTh$$mp#h6 zVEA$smhNEO*PrL2MzlKv$_N3m8o6oC^t`8xcf=%Q$MoRJGwYzKro1lZoz0KS;X)Ve zg85qS>n{8cyfdpmEmFXu^RD|H5F9ydtw_XEW4~e_q0#W(Y-Vqq^q|cxS1-Gg$|W=+ ziD+O&o08>ev3J*!xffgaK?|pI;Nq2ll?)~#wPOgO;hXcw6t%+B!darlu|C-&(l~6P zeO&dEI^HLi%jIwVw+35RZ9{!RSC=>ZUqp$P_qHbc+fTo`>Yxm!T`!(qdAzu8`$UHa zw*FC=0nO|=2Q8^n=~3D!A*XDvL2FUK`#hPJWR=traK1n~msZ^Ae z6W-$jG-Zjwy;^n0Ch*}~)9}F-mZ45{mWoWniM<2X3;dbz!Y3~a6inh1&c~E`gChH%qHb*4tffHRpCkM_;IJyyn7o+v{8kqs=x;&T@;;m)=pidUjWF~*T< zfXCdRf~&4t1A^bf*Md}L23cu4JC}^;z9b)|+Z7G_S-FezaNFoIy$6{iW!^Xk3(?0k zFq=5x^Ek#(N;zGpsYHJTGXzih+7kK(O}M27{nB^JxJ=Dc#?4tF7a6~o)5-feT~2m# zyv2kX7?{~F{!PXLzmXc$7O|XvO4pS2cS_$*1@T$N72r7x9;|t6ZCbBOw9cgaxRn1IqCpwWtP+;K}Y zsp^jT^M8YG%|Vv&wTyuWlG-``ybhOg)cze5AdwiYh06(m`Z5O_!!5ZJM^-LXKbVI; zs77Y8JAd>zvPrSO55cMVanM}nkOlip!Svikvs$^ogI7Cm&C_3ZoC}g}O?wU{~c`n7aO zojuTJnDwb4@HhtVH(KJty=XBeHJ%_9-yi`&x>xofLqc8AU5^`E@UGLyXJ}D_Y`nP8uvmm&4$z$~Tr2CR zang3@Hx?c1@|hnERj#x;s`Rn~dCPU&MoLWdq(I*7%rX(}_on_9b9u+}H}0>4FzccE z=cg|wAzlxrTiPp9I@>a20X&5`H#RB;Ilk&k!qKrgf&OTp&-JWq)`|-{0h4-do^e6%#6l1E{%Y&@KFH8q zIe8R=(TGl{S#&PhWxz11J1T>pGeO$1R7%(P4s-hGp7e<|A!}+ibwDQwf7F);ZKIbS z=AvKW@U8Ju5F#dH@ioN6E_bfS?AycZc`T%a3T3I;CyX(|zVBk+%=Bx0?wSe#N!<#1 z#QRBx0Wx=}ywWaJ64<=4^ITE{lo__$jHD8E;zLU4aVCb@kte>CByf3(_K1zQ{4wqF ztt$OsB<*>3dV^oZg)CWjPnYmU)svnc?4%tbfr?P-SA&3?S6X- zlB?Rl+$)|ITTJf&ykS&AAW#NH)~?ScO)1NDCzFDTbkKs-0{=U5Lrh~FgokiTBn{!4 za1T(zhs7xMQVTrJ(w}NNSSzE~f|@0C>l#O6DN( z1zA_P;~sVR#gA z$5la|k@krD=K3}svh&5GX`+~~eBlM@XvSjJu7XyZj*<6>n>f~$pW<{}{4&&kchm00 zxw{wH_jv8msPo{`st+W?o6Oa(AxPyQ+K*7CElEpc=}{O62FU8tialz!8|o&Rwe_*9 z7yjT?W4_)h3U;(^H}oykX~9_%Mu@9GI(E(EWu)4U-Zkl?Z$p9T*k_#BF|=QcGFI{z zwKU2hGDs++dNwVZM8HH7|Bn_RJw}bSS?KiELwTP{Ffr^03&We*?za?2QlLUMJP08+ z^(k-D8t?LQ#w*ERV1LI_{vOS*+@4jNnxv8-Fy0fPk%FYPHxfv*`Z@+hDWc3be%c{` zX+@2@1hIrvE0XK%k)kUDewM6Tv_Xc)mZt!>=O(dcamveZmTLFo&4@jgAkdWyHQ8GS ztcjl{-S($h}beSk1OGy^@JwFls$Px24XIDW(4klh%^ZcEJG#lZim}q z043yi07NzqzZqwp86U@rt9tBPe6%?uNDoX^NxuIN+egL+*}(dy*`xG`4y&g3<8`1J z<0A!Cm%GoYbq73B$6Ba(#y01VYv5u-C_P0*Dx3~<_a>OV-0UuU>zP&=f%r?PeGI;k zqDulonjzKhTrUp9GIY4pXS?(YnuQ~ER&TDWr`U5K@Hk;jD?Qzqv+mA%eej}yY0giv zZ>KJ<#)mi?&YDLdMg;lc{n0N?DI{IH`%|6ZBpMD(%m@2i7FiFVi6?UmSoS1E9*ruRZ}fXMR?$%9(JiDnEJ{lp z0WyZXB8rsJ-#x!XAmfGEDP4)4Yp|*Qkg{J!%Pa}|R7mue1SBP|jk?)9s|a#@=fV;Q z3qg{>6V+0Ci(ht^(NNbL@p3ZMRzbvST$kSh`s=id^+7vpZvTA|ls zqZreD)2&zgc$}>@t=h>R>cYUieEt~Kx81V@=}iHc=3KQKG>`5Jt-IDK5;U2Yl%rmZ zxaV}_2>dVxn~A&(NdtYV23A14791H~PHvu}r-)>&l)O%Y46^V1&5t;tkugNdaHGrv zsug(a1r!)HuDNbL)k3Y73SZxnOAr2H9^)Ul|L8W9s0#KA@a#3Q8(r9Kb^Emrpz3S>Hnc&n~gbkWM>U+$RzU zcbiXN9P!W{bF$X--7hKT%|vj2`vbW91=31d`l069yms8nF{mnz@dB!oa6aQ-Mgd)U zVJAYTv}BpssM$zS*^=B5j3OvW0+Llo3>Gk4Y7=O(`kl~bGlUJ~HumZd)1R$t1gqU&UAgrT$Dt)n}>wU?9YK^8Qtr8vsB69KHyp1!SE**%2yp(m$8h>)S6Er51j% z@~+RqI?a~J6uQ{B>oP%a1aK%qoR*c}6q@igVSUO<`tZ9@rC#A(puo>UStgte}2s)ago1aHc=)-^Y44}4=!uQ7?A_%t_-bK6+>T~p-8gNp!JOH+! zeKQ9>dqGR|`$WS1aoPdf6?cy3I|$qq{Cqw&W_s~FcjFx@lPkOU2GIC=NfMd(H; z!a)1JN~uOf`wu$S0wr6yPr|-(W7R<33A@%{Klw)Tmc88dwGPjkJ~?^x79Ww{M$YFJ z*blFrTNMhQ?9YA65fh##Dgi>;9PD-Oqz-(u8*eDl2|ym(9!{YO7xle$!4As`Tm1qo zxEaQEzt2)VCnFs1ZoupnQ)a_?TJy|Qm6X}2VUL1EA!{2;*pjL6ga9ds0CK>3K;IYy zbh^Tv1-~b`#XYpK8CNSby(_fqn~FB7b=jEey6r{h%_ykxh>$9&JxD~^FPfiS!6Hjv zkUB2zblm+wMP4MsseoqtPKF6D{}c$6w$>IXpF=IhaqUTpJfS&G6#K zl|S48PVav*Ac7tA6+xXOkOaVQe4%;OL!Y^e1Lx~Y^t#N{-&yzn)Xco{(*KYnogEwi z&hj|}I(Q}5g$x9COCo%FaElBz{LXW9Ll20S4tE6qS}wh*f1FzueN8In@XXg;B`)qV zT};jTZelMDtk>u$pNn0WztOKqBfc zZ4Or8BSqV|B6?A>n#BfRf9nV9=w` zo?I&q%PdLPt2t0OJO+ohMd6oKR{(;O8{J*6sQ=oRolhghQ-v>lbv^8ESR2inrShBY zInJH~)}odxHp2s^y^b|yt0iLY)y|^g!R52MV6JVSW(Ywx?kR7<{ie^R#`c~ zQ*$v7r!Nh8Z~G7NRBf+Y$U7wf+GKU`V;*)F^yx@WpaV|-Lh4ej-sNVh3 zTkFuf9edk9^NpM44H1py5&6gAedHUD7HMAPKQy1RFc z-3)1b+^mUAKN{BfH~76w#RkoOA7q5mxEl4^b?3)1>%IPr?kp0of}E_daZxY{5I z&x%!@%#4C8|Hec+wImK&x%wr_>OB1pS={_mO$day2`NS1OTZdNVR%W>AA;Tf!*L}4Y(Y+HbIH)|jS!Ae0 zpI(8K(got*wD)+Q_)2h6GM-Qe!*e9w&f?TqX*>zI*t^OcwQKlY1GvQ=U zu-4mdoJRNfq)=zblvI|(r`~dDBxW1F<@$a#>MqsFuJ;1XNe>R>Z9qV+ggSC~@1u2Y z%fo^@cm7Rm3jIYQ)!2w9`WwY3R$Fye1B}aTasXzI@4qQ|P1(t`UBLQF0_$=MRco=c zF!e@Mf_hzQ=#KvM&N%P$QwT~s<`g($1V^#&o-5FRt5~mG)Tw4JYWXTtNjI8+Vm}d3 zBZr!O#tcvH&yY7Q*7gh5^uZFMiErHo79IuDepcaQ!T1;2kR(mXU3g}9ic3J4DP&f- z95tCl#<``2(3O}yKvLY}&k4Lo1J3FeigKibKYm4?Y?LZGwElz_&7Q`Rk|xBD?isMl zNMYWrzl$#ZC4kLgV@v_!sHXfK>(7>Gx|c7#v(MBBNT1wP9(*C;FFckDM4tAcM(ftB zm2tj9FL8zaYXA3IHckww(p4|;xNR9O`W7H{-xAt8fBkzs-b%iPz`j`a{`UT7h5Khl z{R?lqrIj#zRpRU2L@&S3frF3xcKBGo`|jezYs719jcVDP?@NT)$a|O*)B*c51YO~Y zXa%LK&6~-2WGo^+K2uUKhfxUD9Q_KcKT?uoj`8Sli8jGaPu=kiX!3i{nkrTfg;#ZN zwLyu!9iSCnApjqZrR_0*r^XI)by$dZ66zZw8 zp3{;CG;DrtkMSRX@K+2jkB-hjKbe|lz-#5v3ptSxHE65SsWWy0a?ARzbktQpz7ZHR zWvI9JAkIiF<#AkDlTFz}^xZ;6EeA z2?M=qNzWgWrlx4*qIukzig}LS77Qm&TQt=9L!%9etF=GPXS-u|%?<42m!k5Y<}juo zc3k`{SbK^7w$>v_GOW{R+b1Obl<@Rl{vfH)&a-%lT}Z<;&{bCuORFU(rZ=Ux_HieYOJ9A;cA^kFb4Xo*_Z{YrP0o z`t3?`kNV(O5L8)8sgKpB`Md+6&|x1JGw@`W;vh-(G8J%O_xaLWRiSa1)i8t^iA?0C{2wyZGIoS|>~26Djm z6MxNW_?g!tR~}-SY-HCf?b*?+ed?2K&53s-K<|LhW9n?1_KwnKu3cqcSyTC=hC%gG zIdShRGi$k^A)a}S-Ka$#^;LcC6%(BTDsHZI;KS9;Qc)l(mECeBM&5prpR137%=6!l zd=)A9QQ(eV{*|a@&KO}eMCje)Q|OO3$2eA7 z#DLb+-4Y_j#Q;cW=!PC6A^0R}X@prv5(F`@mV0eF668zs>thXb(W(QeJI5S9Cj2lo z*a3{PRiN zS<_wqCL{TBV+~{tYxk%RcnSp!%#A8+%u6@j)CmfR^?d!~d9>ARJ)`C>hG3!7V~$Xi?bYR z`mYE4e+|Yn#i&le{B8gRYwflSl*Chh(f+0mek!1A!a{x7_@hRpdG5-1ICm8Ag^1Qe z!6pELTs;se2IVKi=nAMTkU-6~vaN9=)#&O$E;!*lvENNb^78=%k>ojOFX;y&WSm_9 z`hIEs^{k6V&6pf4HQFV8!JXV9GX;1GZJ>bGZsGblxEDcr31UMF&_uUOq*GEk=U^4w1?$A^h0@V~7EKn=lEGw-2u;fhYJz%jO_xqj){ZoGGTdl_rMv&w{5&8B zYZ4SFWcx%#6$~e-itfP}eWeU-=$5++fsK{T-bVN>QWMi$!@6&?IO11|Ne8W}=NsU} zM)bJ(P?A}quS+r@tdgC~B<4t1_kB3H4;K)dq(TqE_ir87;a9vmZ7>v%a1h$UT-&ti zGL}a(fJh*qVxq?+&7;Lfd@%$0YV$UYdGKrNE~7n$#TaUQ1g#|?Mj=68C1tS96qk}y zS+S~7|L6m0k!Ra?pac>eT*nJjy@>pLP_%12-qouE7~q5Mfz(;XqzO>8rqq z-=6bH9QpW~polkr##f9oj*#A$tQ*F<%T)bnc*O0O=?|{+N5?hW zTbpT^YVv|VoPck}J)(lM*hocN*GRY+^9!%1oN_YSt8b+Q$o^&NJuD8Do{SW_MZLwwaSJHKxkU7Zy|iO1zV?1 zynKD`5opsep0D4?^GMYFnAQXnJv_5hd=~K*hcL5OfoVCXDVv`~L*HNqo!Nla@w=BG z)*aw~*{aXoHK>i$mFqwRAkg?hb^Fr5hHM-9;+b^+cCg9)qBR8zt1o4pgy1-zt0^}M zlQU-X;k<0`Vd3W683>>)6|5@(7V>>?A$uHl+^QmWku2t>)m`BEao)%HYpe*wyZ4j0 zd8PB$;g3&vsO{>ENs`p*0;^+gGD8q&Bdx1|t4f4|uf^;^RrHdaR1wYGOr0YNkC?J^ z#&d<2thD2}UwtTFVj{&%`7`y6XF6bwJp333TMEJMF_w`ScLjtt_&8(@U`a@FC+hDMr+PTX~wI82RO06@lf*=*1xho>K(5nW0FQebwXYxFmg1> zDCi3p4ao13Gbsh2x`5Osv>iT)G=vS}yTC!G;tFl zC)NQc!4T99OmG+n+pXG@ z1BxjX^6C5GBn9@?XywPV@{rimCxD*;zmyjG$-wbd&`h;P8}+1i-ff3AyWk!MZPg z=)mZE5osLK`ai)q#m@j37YbzM^L&Vw_Z*ZZpd1s)j0B?Z2F46NxyVvK=Fwu%pt29{ zac|;7{uRM1TGJlv^5Zf#wK~{v?4BOBxz)P=*d;C-g$J>Q4#p>ch28QEh8C&Dd`Sj+ z!kNoEmJOtcmcoKLh4K#Z1Tu@2F9+SC%F zd#iH}->|(QAm9Vf2OU(t32E@>_)9HDTfAi{PJn&T}d)# zPxWS(BN!=*#uq`EtM(vmlf8Z7W7XhEpb`Q9x-OP-Wu{ePk84C7F9pofBKyo#QnbM% zr(i%DPt!Xz&_AA2M)l|I?j07$BjM~Jw**dtOCBsu+|cyOQdBeiRDgG^E=eu(Se^`a zE1?U9yM7azzSFvNg%e*0g<~*`L;Jo@WjPac$tllDviPO4qG#LI0}J92hHZ}Y#%Ze( z{PJDp5$%yB1A1Cj9jxx9rn=}`ZRi-KZKFHN02sI5(KzumNaZwXopVFpc%P(1p7MxUR)5;5b_8J z8KOPpR&qOXz!=|W_p1Q%^%4v?8zM~f`b*m40PC3wQlGzJT6Q01KP6H-g%+&Kb1d%F z8$YkEzqYQj>&-170S%Tc;`u3C|7Q3VM3v{4lq%Xi{EEd9Gz9wk9zcI;^WOIQHZYU$ zBVeSAt$lC(xQ2JF(`vfw#-ocynBi+(#yDXFIN1b?BW$%EznT0qP|r?7JMALz(nG>B zamEg3;D~OQgmy-KP$0ZnD^ug{O$$`ri60n15pk<~@+tbQ%n4nNN|*KnYtrzyKAF={ zN`MHTOBMo0S!?`#icr2%tBA*G`J=bZdC{B4Hqk8P9XFpcNU6_QXGjk66G=svuqv(QXlDnoFbI59rTWfur4|dvu4W1hI%>4=By4)Bfl=VVI(7oLD$YLervB>rpVeYlr8P&YJes7@ne2Q#*7Yfe>8T&?bOgW0{`kpWug8m3-t)}VWvKYJg-saxPfbBTMQ*(vD^475zp2k(~QeE5^u*s>$d*$A9J9t_tt&s zeG(huX$a>gcY#hu4GImt+wV=byShB5w()O*M*!UE-7QN1`GTkE6I7)Vfe^USzPJhp zF^C&UIk;NuITb?WMmTP61o!ubf^dJ#YdROn9pZQyn@=<3>QIlzCC#NF8A7q<59!~3 zqSdV(%a~NW#~yhS6Q3NkYH?t0PnpGJD`80)^j#)u+W2-3Jgdk)-#WLWerT`-1bXr< zL8gBHV0*7p=9k4+inZhVUiuUmjsY)3e|69n<-e2vA8~{K!u!OaL-EiS{Iev}3yxYj zqgu(rg3*Z@06?15Jb=BTv+FJT8G$;_YT~RuXEG}p9_rii)BxygYk1<>FA?c`kuB|) z+LmOrv3CeEa_b|9JCc+4Lr)Sg7?7uA&>>hNkAQ04Lu!>xg|W9zZ7L>BMs`uVjo)DA zlg7hfDYUy~D4&QN)#in{@oEfLTO;of-vxev_`Mf}AWWAIM3`#$-ro$O(v@4@x#~nn z(WIz3Vm_{doqfe`Tut=*ckt596R~(WO6Ii6%WmnD*&vTXKNJj>0+LgWZsDh#g4D9J zw+_|vmW3I8FGB{?Ln)`$%7Ejgk=;Pdrmh8|TqW?+O3WSDTkxm5TgwQ-o3Nn4E7 zqPNscjE;(zu&sj`j{yYI7Gw&h)B_4`Te2|f<1OYC`zmYcowopSp#*OqhK&9wa@`KC z5M84El6H_>aS#BF?zxEyi3myan$MdIEjAx7`cn*3G_ix?WD}Ahtg#bH{4D$y8|JDw zj4oCjvIya)f@XY0iGn6H)ltqmWH8m#jL?ePGvAE5P=kOFr}_*EmGkLCTc*+ds_L%q z&Fpc>)UE(&`qv2;x0&h2&{WL+o{j2nKD~z#GEC2F7_8GZ{rgTngvA|=?oYy9F0x;1 z^?$sn@Vn&!e^ITa@jIoq=3z;5;lfBTwgw*LRl-#f3zD+?!Td4g=Rv zb7o~HWPQ{oFT=KLdt2F4m*ZxiB}eDpz=Z$7ph zQl$6!L#;Lb$aV$B1};)Hn`byYD=7E|z3bbX9>Ml>H*0P%T?Ur$mnWqu6tH2`^s<-{ zS{B2A^GA?>VW{XnapB=h6jx($!Y$KjHV+?_Qj3D`ThKj>3%X@M$*wg+Fha_qW|`)j z64&;~LkHXQFTRCaz)|iD$&yGDY+Ja0QcK5zliWhi@zZv#AA>OP7o4#1aGZaW6DT~!~%uM_;MUC zO=ZC;Y_RRE)^wl&O7*fRL}NCa2KrXaL7L63A}r7$^&r4|5wr&9;c)x`dd?L2Vz200 z`^x3w?nVhw!l&64K7aNeL+DWjQ4e$C@&t*a)%Zo0*bvd4Dji%M8bS3|BxcW09(w+6ZjXFoaaZ6WfjpG;Fo6oKiAU|$|~ty{Lf}D6i_0_DErDW*Y*Abu7#9GY=Rc9yu}$Ea|P=W7;OKQ9Hz<{8 zdfz5od}}nOcW}!H{S9$Jf>{N15X%;sC}I>740bE`#jmAQ!3*z1Q;>qwwjb=FZ@&S$ z*ocEI%_m&}KXS5rnb>2@#$P~`0e2l9_~KG$lAB=gG$%m>0>Y2lS3zouV}YntJ!(JPl(8-;feP0TeZd)_92CdXJXZL4&_GfL#4 z{(lymQ66|sy9(T7%Gen0b!v`t5sDRfWmD)@Tm2;j{75M75z&mDS2b;fCU@<-o;lEG z;!j@L;H{expTisOu3YMJ!K#&2DAu<(=U8`Q&OTe_NAOxrcb|UG+?DN;mx|b`_H+x7 z<=))?lF6-$-)hb5+!7Mb7(<0zCh<3yq_&AMGk9#(tP%+o7eTQ1HaKP&y@l0@b`ys5 z?$am&{bqv^e70W<`DP33pKQ^3bJf1EAl;~(3@U>k$50r98m!b=G3J-LMQYdsT(rid z5K?nIwh&of-0pi>c`_QdHdR&DqU0jv)v&ai$=0)mzbnN@-2QMvoW-C7AymvDU;Gq@ z}DUr9Tvb0I4})Mzh1_=7Ixma zYRL3cKtrFNsSCI4AEusLnIEnD=cIZCe%?y1_lLNMNT@M3;=HA}e*}J}?uwYSm6jVD zUd-R2r1@^`xGYF2^u7NOLZ@G*;{7wTa8SfE(8>q1g6ph|iT2e>D6|VPFfP?bM+71` z2BH(Fh_xud`E92!9}vBEDpI&#phyxi>CII936IR$|LhF~KL&(6s8Mff#7fz=cfWSw zfv#8eQuk(D$U#Nciss!3Ea))SG2R*b z{4RTiogZwhujblImfXl5{{RjDff8vIb_kkmul$KY56FIYW)xNY3WwLcA)bOzKTg3OJ`i7f3s%u~>Dkci;5j&*3 z$QVZsd_}zD6K~|gf3c|3O`cYGR}1zH3u#8iqC|+^+S`Hb z+)*Bx4pEH6Z|=#hz8xvEGy_c^Ch9A8xUc61{sz^M4IR*vmla_cZDBD>{#kPSGY56s zVK+S+y{Ger(tSts;&ROL%>JSy6W%xPO@YB#x7(i~kYi|vJ^M)zI0Vajd&a4OTvP<_ zWN2?EulkYT_`<(z$l}il^jRwQhQ!q=mjA1l+e#Nm+KgK3~=UrA-Vk6 zl}S5O@dKl<(ID$_Xw9k>?FBJ->X$fcA&w(xJzdxJ`Wmn^2dP{J@;XRYeQ80eKYETg zY%!~hxm5UP5>~>!il0Aau~gFk1u8dRIhu3!g#R|9#Gb2MSEg7uxMiDo0!`#y zF^4ND*^%ml0JFxNFdtG3e+j{W!HG^ynUFMu!my$9JWmP9asgduUXkl$uy{?Gy+(il z-(da;S~udAnL&OH=8WXrs2>`72M9FgRV>4D5zxLX6Ws=lz5~%YWrjAKCvBf&O-eCL zX*?r6kLQ0feYrecBpf}XDEG_O9Sxg!cLkKu_wDk~i)5V8Kkz){+;=@`wqcj*!59JXacw^nO9APCLTS4_ zxgS0{$2n(|kV+P=>#=6NY{=tS_?ftZD1%q5-D(iZ;dDW_<(_P;=&v7_#oS600 zT?2+tQtzOri4oi6F7;A|^|nf zM2M7uN^hl;?3+aByM9@J`g*0D&a!Yf1s^w-rQtum zPNkJ;X%OHA$$ch;kW^lwQFDxjj+ubV(Y*^etT@L&OVBzjHs!fe3 zQrF>O;~I+iIDZ@`nqgLyZvN1BzgeaWMX*wXoUA4_{GSxCw9oODG0>- z4)e*GR1h!9^qc8Puu5aqoJM}>Wv6Ub3)p`Gw}U`8h*E-RrN3qT3J1M{LLN;+93^-= zsH_14`MHNkhNNArdeDts>aGH4*TQv*6}L~uJFl|@G)8LEe7B$X@T=}v%fDwVC!HyA zcWjL|)XobJD<|0&K8b^4;Yii#ks0?0jdFrDrQcg)YJ|0~8ta-_>oqB-W9kw#@yYl5 z#p%Rbb$>YKL%kx<>C8)s9un)6Ywkx(g`yj z__rY1De`DS^P(l@+6Mwv*FU81IzcE(INRrc<{3@ziNpGRq47L3|G5AB&X|fX6;`ACQ{iNOm53bI zWva+pgvIscwQ%asK@3=KjOfe~SB?9Im4=RhPTs|5h-F{Cf%!SW|EI zKiPM+!-pwg2*;>#Rc{C+ucM_HIZq7VyIU@|ZR06`93;M!Mbg@YI!TXYwBh^jKy_|e zYw+WJ)?Kt7y*i}}#>rEAAqA`IO2wE2SI@*Qv}ZvQpA_HUs80$>FoL5PuPl$|Rs&I@ zf2RQDl$bLtFSy(Ej-UNlumm^qo3BrkdC!~BgU*ESx4J_F=(#S@CMu}rKS+2U?$ILD z|DP+ODCg@sc@zA1o%~OtAXV2A zC`)Ir<-?UBNZ!ScArJz!27g7{If&`oPiij8rcSWb+c_Pn(GL6@#M*A z69ULPrx*W(@qE&L*IloeMu;^Wg54d>! z7OVqZVd%&1>E>=E&tG3TZ!Q~;ziFjCdpoN6KqiH`MV0BpQ!K}caUG`8^{iT@Xo9r~ z>E$hQjHBD~x-@oVvXz5q*Jh->%C_Hx6fx0x+KPAU)^0`@d7y9lhplSfe!1qOMW^(T z+boNNi1W=cbIG(EpozH}oEGJQ5JQA7NyRKe9l;N+G)>Rrj`f!$!^I;$B!a$8$-G>I zfjS6Y4l1vhQBiMwf3bQb$BbRi>Z#76sie9T9T%(Z`sYrR`hXs~@rMGJIKS@D4#A9E zf`~eOtNm{;~?n>^pyKav8Rfu%> zgZu>w>y_?8P`EUb=y(uXfvnm0G1YVw%+3xv^}%boy%!r>G9Bcs0TR`fE7nT*>x#Aa zR59XhAE2_^-_o6b;xV|$K23WWvCyaxWPJ20DLuS?U!g5%cNfT)y(wSi{j2Ovr0+nl z%fdf6B^kyZ8RQV&FdqgeD3B?@D0pmC^T&^hy*D?`610A3kf@Bq2)(I|_1({14|rI) zw+qooMMng!Jk%7u(Bh2Xs--ue({a}=P&;_Ax?N!%?>8jHqf|dzO8A#tg>2I$N!XCE zTk^+o=LMI^6=6>cG>S$Vlcb*W1olq&42xv=COnZL$Zk%#Knj09sDj}}@sa8(2bGKM z6SPvj{XwUnauQ4T+zFQ+Y>>pI2Tg0yZY$tS9_&=gMlk0vWz!lXqs9sTCIYl9ch7c? zZMb$;&2V42rg~BkpB*C;kH98y0*O&Hidhz*1eplM>HIyj?Cpur4e9{CwilM~xf=$V z5BFe9Xtc#?=&Gbon(rDnw#%}qVAuRZ#vVR#NRIba#&K-Jg49Gcm_n5GWeLhGOLFc` zSbiFkLU3s*+zCZ3^;FT+HuzmdLMDR0(NQ+Dv1bq@S7#yoIk8l)b$)w1^MKjb?W!Ex zsn!cLC4o4Ohc{%m>%_O2~OG@LTvyzzKBqoDwz zv{m?*hG4h^77xTVX{rkaRa570z8B#YjkSSsPI~2LZ}VfqO`ecJ3V*^&iz6n&8v_92 zMBr`?W}O*SqoU3(-iWK@MZ=wd)|H{ivrk{$MInd+>*L`euG#84!LJ>syew+0;^dFQ zXB_A|@z28O_evUtaJP})ONjE+S{li=pjj+awui03c(hrioS#mT=v+w3V-DvV92h3X zHL=wK_7}wy{AeSlWuh;9(FDQIzjNShgz^tTG;@eIhn!|;$k7(kfp%0<4F?Lq9;wcn z>8k(F9{G==#{=qhgLGK$`>y(5s{a3_NAwTt{1f8x0u3z}jJzZp2uX}S8OMpI$5Q}F zjfNV}#j9oh(99huIrQyd``pLpB|5UzMu!KoJ3Gx)<+x(_nBbtf_CtL>A(s%3Kt)^J zePnnz=*$8~H6elSZbInBRL1(=Ui2qqm)*3w5JD-v0>fs+Q@AuoU!aeDtUr#zMzV~1 zJ`Y&pd1f|y9ip_znP7}aeoW86^$#sM87)e{=hf{DTUOfrK6?nONG!OuuDm&aT|HUx zgDn0oL!%jgF~qIpu*PXY_PN0|2>%DP7vjn}R}%$W&L7QLjYyv5`^Of}UHm(Ygquv*FMt&8*tLLlpPjEK5rJ-up2~ z4Ccpbs&Qb(ZkMl6ftH4-wJzm5pZw={7B?*(Ldw$g3Pu4rCQq;s!uUlGY#3)Pb(E0` zGf~So@$2)}$R@H<-VQZ2>N?U*)Jv<8ojQXO*qReVl#7HA(N|dJ@Ae~e|8VZ{CSadE zx+LL*bA~#ZC8YG1r`VHxg@${1kd|HRL*(kdeFxJvcudm-2t&g^e}?{u^{_YAQb5uj z99@MD3YthX#$0ksf5Ze`4ni(3${(#hKJ;osgOCn(k;mKK2=Sm>ltYTOCD}gH%JUrl zs&TUAzA?kRZMMj0^SP?5L>FoH>EFzsN@PxZfTQ9q^Yz6V$!Onh?nPKUzNH`d2Pt0{ zc@toEu}cyIk7CeYM#N(>Sl8~XyS#(@=Q{*B-p69@z`*-o_W{*E`AzgB$&Q8PK|5MoUdV`Tov21JThb#U1X+peQhBfZ-QqYy!qQA&||0^?Px| zCrpYS#}?ePZE^RY-mpA&x>N?MGf+F?gVT$|1nuEsm-;ajn|hR*s^>1|F8DLxDRqfh z*7S_UUU!c@?c zh>loq>6w>IZ+4G^B$uFnM*0W~BatW-;ZSarhAOR+;6vW6bLk;B_8?^u&1enws1`t`DQgVjkMFqA$+)U zcdcbw5K?r0l0pW1cNmIJKlv2Lb_8x=w5Qk&8|&)M|6*=&4+%c#q(+O4-ZSm`;aE~( z;zJ4&h6BBaUL}9i_C5SDF2!bv6BLdlOAqa+@Bzp@gp!3hM5egK1=;puU&W~PvP?<@ zDhQ8~uv|o*&n$BO&Mpz2b)bgGD2rG`1C6OSGs3>$qSFN$oRSu^SD~Dc3#VQqBjS&4 zc3buZUXfE7Iy)^;rY)M_%XP{x1K~%4;*Sh?Z=sarpic|CZsc+}Wgnjc)Bg+(TG?60 zU-12ZbiGwv)&17>y+9g;MR#|Lq~xMQx;q5vlrMpwQyF>(}k?v0Ee*bvwz3=@# z&vT!7;E8_L`mH&~_>M8t6MY1*tOORYIc3tQxdc1+Bi5<=QvSro4)XcKQVIKM#>mMG1AUa6(-=r>;*l@p&aC;j{Ts zmWu$xMf~k9&s71aFTB*J!cqO_aZaT?xa+sKKh7FNCkc7B`UxJ?fvZ!=RR3{&OS~ZK zjwVjh@J;DpU=ffGw`U{l;o;Z3%5KC6T&4b4U*Z1EN5|>NL@=OFN-q9tx>GZZE-!5* zvPD1j9#(4BGxPX|)N(3sozHoSI=zNNSeLwA6xuL{!{oyc!Lv17G3uW zISw}M`x!2O{^PPc(5;193OCFW?NgOYdPYmPk(Eoy(gU{$TV(bP1QWT}OH?~Oa+i#6 z?8iulX3ryAaChL0B$L9rX1|nMpsO0S<>}+-54v)MF?xN}VsmTI^>0%V$^h96ok6|V zkaL1(nUK1)*u(i5D&Nz+uF8tSmZFA)Rb9u!R<05V7g+ff)rxlfPu?yCzGqi!x$n3G zRSEN!_HvJ0(4M+C_qCr;FSvJ-nx6cKBxIdVR#3j=2zIk|wO8?7&z_BLb&$%>9cNU# z?sffvbx4unwY(V{&%Eae%BuUH=6)`um+gZW&JSNz90T1-yAP3=1y%Kafczjb$VsyN zPm(rNsZe#X+8rS+Q<2It25SoWBZwDW%dx3S7 zu}LeAZ-J$+zweKd+vkZn2QbCXy3{>%HwkEuX03ULjBqxA!v|w&j1g|zHc{H*GsM*7^meTG>xj=~Iy! z&e6p~3NW61EpiIqKhW}QmyriFLW4OVH~wEZQj7^pg3hR_eRCKvH?4NA{eJn~og*?F z6dIK7kPX6jCaa)z2vu}A$GKNWOC}KKB)AF$=_0Ih;Q`f7Yk74w8#1R|t^Bm?_mTyz zpFs(h5kU`7lDj7v+pZPB>MP`Ya7+QLzSsZw)Bi40_}44u$Hff5-d9nrSD&~4GXVY{ zFPs^~063O8OVJ&=ABEuBchNr^ovMPX@@P`CeesK%MlM^4=oj_XBNg+e*n$9YoW}2h z;%u%}v`kcAHqfd)ma+-D({D8agxR$i{b0;XON`Gk1D9X#k@s0yiVtQ^)@`_=8Gc?v zcL!LE6NM(M;_0n8{ei!ihVI2W9u612FQD0epv@AyO~2a~d;eM1IRo6V#aq}w6r zB=^T=Ya{!UtRiqH$k3g=AH7jw?p5)RK7GZoB8qdHpdjsUEYP{i4`b_xZZv{fAe{SN zaK}DcIzBC7eMTVT-S6za&7GgUA#9rGe`rn39kKus8!NyG)bb8T|wZeFblU%a|Bye1)#E?6dQ^~r_wtY%%^eRE~}&3e}M~oaJpX5 zqiz#eD>tx9)e;|P5(2|lw^xnIA7H{ubFHPj{`+OeiPy}3VU zI>_=NKFqq$z4WF4S#GZvG;bkqGsF6}0Hr`b(>jdlo@F`z3ya_h?TSL)VI(a@qX-Jg zm_up+>>#}{5pR?ul#a>gk&`GHJ?lYTq57DSHbSuYOfm6mN;*ju`b{6XK$#~-w``e% z@kXNfoE;Mia zxH&br7x8sKKKBl*KtCas2DQYMSmuCLR&Q9c;*h&gz8qR1rM$G#>kou)zt|$bTQ(z* zTL=-+qnBJS2cL29;`^)iLG~=Ek6F`dy0FKX-}@}5D;-%FaA~S%Eo4dzm?-@%{l`8E z?r_DDhOqond&rT}OpTzh&uN99Qn}9O+|V$!Zc*PTlC_6wI4DY?CGdxsOh$rr#?7rsc zeGuLp59ENIEz%LuiOdFT2UY#yhgL@GfeK&B-R~@E$1IF1y{J9By1f87x&3?#* zR?DxH_&VZKRq8vIp9etOhzN~W^ENk;sI?+_w=ouyH`t+08~MNKL?!i99Z7wIG=%mw zZ&~(+NCYZhbCb?ih?2VJE2ctWzEj3Y^>^7wue(#&@FqN6AD)^xY@#$Qd&>s8s;C8T z!9}AOCCr7$j3N-j>^rjO@ps*r(JT&U?B#`4%;1(ikLSb4INlF+fLy2eYpLZPU~6XD zy(c{W>%;Z0VQe+(g;MOWpVb@vufmQ0{{#01fl;6PT8lK97oAa(-QzC)Kx4_OZ^pl^ zR*@_{mGiY6ZRj){^6e1P?NF}HmWKdY^`Cq@;x=l(J~^l5D_G#l^oL0{!x|7HR9Op5t%J@zXzI>F%1pO$&xOc_b_ zS~TGs4Qfl2TGx--#KaI(fhOyc!*?hotMr#!Is5tfFw70}s?jBjzy(7Un zl|~$~NuX7<=>Tu`5ho*E5tTf$eDhs8deUn2cQp5XYP+=a06wdt?k(Drr`n5vp)mN_ zbZYEF^rH{V1zOJJxn5~}M6QOe;~zH|cBoY~AmeP&5!)n3O#4deM0VV!+Q0`Rtf#N= zD@fT(i}PMXn{*0O`~=4L(Zc!EM3xK1e9%NU2RgeT=4B0yy(yo!sT1u{SF2oPUCLPB ztWQ<4WWqZFz8Vp^iD)hWcDkeCYUcLPdK z;6cSD`WRjkY@a()<>I$_ZmB@7lvy8O9HF3RM)?BA%rws>?5(gf9{3Ts%@dT4j#6#p zHmcWdkw7dt@UQGuh6%3Ptw};8qbw>yi_A4Qc5kWeRtZ!XBU9u@;uly zcv;MPA>grLpOUY0^)l6FDlA{>C>R(!+eygJ9t9U;QD$qJfum+j#hB;( z^VOyqzD>hO=N>hJEP|I8^<_Q|{cSC_FsP#%HI6GInXeGBt?usQ{ClShU%cdv}j($2#c)8r5xZ{FGUI7)}iTS zq}^8U^bcV#ph?@nRXLnFm6O?=S+)oCn*M{o4)7dY02H8OLPI}U+${I~PBo0gFN$)n zfYc{lW1T!^uK`<$#mNZbKTBoA+s~G*W@_WrqvF1{FdlSYdW==BfwE%m3uA)<1%Ah`w}! zzOh|iiTxwP9E^`yyP3Sk%lY^`#=i8Rq2f_-cZ;jGN(>={7cD|xvrt)8J0`fkj~;~C z)Yse5>V{C$Fx2_*u6*cN681(x<9u%TGb`OcEXH5-;jggAOj=EWqks#&m2KbhG`7n1 zdRhMj-ro{XU6Tz(B1FDX0iHIU9SPK7`uNDRg{jx(+$;*fE_xk(`=b6|wfz4h>1DhG zSa`z<>;BjNtO{H|0-T@i!658tc32p0tGo3A!;e!YOVV;rxX=$3H&AYN1_SKP`yS=k zDsheP&k&$AQgSeh!(P(|-qQf(KcBO8G?LCGn~6Z~4`VL?iIGauavZf1dLoWrY{GZk z;RH`|g2KB5@J(vGPR|D)>B*4~8=(F9nG=xT5WA1sFc!6~(1daN_5mlD6fIgA!FyNT z6rNi!FHJvcW5>)(WEvj&2HtmKK`zPNrQV6wwF?Q}kN|k17o~Dz4>SaqMLF;D?V>sX zr`Z;BVlR6`Myc4HfyNMh?ToJAPJ!RNTx|Nj`Yj6jnFf{v@A2>t_)?picQy+Zn;QCS z^_2wcqBK22=99(+eTv%S4c<`*gSX*Rx_dgc{!vYG2z3h64jnb-ycJRv`14Y!SdZZE z-_kiAvUyZe(%FYnj>4s8D?ABx>i09GzxE7i*}lOlk?1MNB&B!U#ROoP)b;n@ujvcu z#S{7U9n)Q2x!#hdT*rXwtg}PYo4C6rUD?rl?86EPkmHZ(a+X}uGM+{aD%lg#~;fhz*hI>nCAZ#ZK2ZJZmZcE z2b~`JRA^YVr!w*#mVRLT7kE^J_}BPJey-6<6y%j3P2=+CZnrFH)jh@TS_xA|y_TIC z*mG<$g-375U$|iQ&mzq5VuhHQaO6>h(5Aa zpUAOj6Tm?N8r(9P;YH;q97$i0-toCZ{(~!p-i>DS7V1l09w2q+$lj4yO_0SPxRNidNJ3nA<96Rw6jt!{zXwA-wR_g*Z z2|w&H1Vr8T)Xb-{B*+M}|H9GvhK*g95zAQAKil4{qS+o3OFt^NeYvap+FG>a?NM-h z2Xw!8%j%^Kax5+Khx^ow9mGH!_%_oWM*VC(d#lja8nGDEx;BRr^tmK#TlT0mz8OH3 z>vs}U%Or!;^}RUSPP}@IAPJ_Sv<^S@8>i!OzWh(U(Z63!y$>*(!(*0V4Q*fkU$WuL z65U_`^Mxh(9=OW1D#;uz{_QdezS~lv&LCdA*c;7sUI^Ap&g>QhdK?we$p!1|E$l)j z%yuIz7g{kUe#Bym1d;9p&0uyvh>$-`VI!k!+%tkRHEsoo>R>&#IzG{8;9 zRITq|A0qqiva2CM$)uD&XsZuV&=%`1-BZaXTPlcA-BFU(?32=XG68~NF{MTWJ_T*2 z6{KgV)NnCt$~pT>Z&R=_gI>J|J3kFIJvGNk%tE9i(l;n&cBzb54?D(Qp?C%cc&o7? z3CPU{P|T3_bN_yV1*c1o24~;cD^<7-E|-l?iq$dIC2qRtsJOcZHLh@RB|xqD>bEOQ z9iIa)UQ1upB$q)BgF|HM1H?(}1f&u%v<%7UZ5q-xe_n~jll`?52YO(Gdaeqz{C{VH z>U1U@*d(+l6q$+zY@G@eCZSfhpBor%I2o3IF>trS1q63XOwQg!???2 z{C&kI{AV^ybupn*6Y|lZ=l-hqJbT65-NrlKEyPH#x}2-X67ja<-Dh4H1D_SH(xH&A z%|PoSoz&y&(1oYlAx>QVKM;9gu=$n<#LnRLGOwpkH;Za7A8YiOZ0US#9knSwL02B*)cqo{FB;Hhgc&6vjLBPvD4pp|~Z73Yt5 zIZ><4VG-%TG&hHiqi~i4%ilK3f)BgOAlp&%c`JI)M&B=Ph%6>4q%=YJophp`Lskn9 zc_#z-N(rZpfiZJ>f61J^)$e1`)>z8!N64?{@A9oDh!nbiXJYnpPU%yMk+*^vPREO5 zFS$ZPtyWmgz8wwd7W%xM2aQz&GC@r;;{u3Hbwq2J10Bz84C(4~kPi?CBSg$9^mjxp z3$W4Gsmam))&T>vf>t#y_dupFWn7aAwhfnmu*r0I;e<=}SLAi(LRY{52_wR|i)~6^ zJu~a{#{~wqvS%!_#8KTHKv158D1!s)tD#np(vVOEwD&4ED@ek z2R&m~RI1HrXp6X!PDE0dCK_Lp)0v0X4DQnj8XP=~Q}q31C@Xc2!J@m*fPvry+qS4|AOuFk`O?JF5n@HPEhF}(oGTo7)1)p4-?vJ&Y#DNDoBU_xe*Izy zf9Cq%iu)ntt1onQI*|H&j)H+l8%b{7Y#G5+gj28bZe3SR25VGN%Z1XI)C{mBKp2WE zDxtD%L90$yAz0GyG$+-m%o}f|@OA0bOkGhIgmbG(#E;u3oyBr(WQP)FW)ctwK5X~oP0=w1MxhgBYq1d>;$*L!YgZy%H3q;&+o(0YM=L)9D2@dBWq z5MMozSp<0c@Wc3MR!C?Ex=kw*dVuAzM~KqV$LWy9;8`?`i1^iS&7D_gIpLpfG*voZ zy<^09a}CL6n3%h4cAwDbfvFl0mC`BP8!*gBHXXKj24zh;p2Hjcwv}X5Gic!)Z<8XN z{3Zt@2(ZK(M!yLU&Pn~kuMbeVA02~B(^iNr7d5h4ou4p#ap|RPA^iuoAG(!E@yL7B zr_0}$qx`(?vTig#c3xNx_f2@%ltC?w%7;<<%t&pOIr$d8N1461$+g_A|8Y?_!H2}I z$JW=DaEq2KCnmu~b7+{xSEAiE_fD(s2m@_zN9g$_^Y8{^HFqB2zPW*)C{|dhZ-;9u zHw&8*bx^JkQ&qIbp=IysuB|XlW;tp}G^p&(AP`GyW|5_#x)AD3sX0PzUHNTDKBso^ z&zh1KGgUubRP*evd(ln4qtkoYMT50WulytKA}>3T-bFzfDV@(Cf2u{z#s4a~+_9JZ z+xDFPhl78XPZnTiETke=fV->NzA{>q-n7ErlaulU4N^0aqQjH?b~z9SwvEQ)%Y@~Z z$a57jG>|@VAw^r7xJV}sGg!!9M-X*%2Gp_X=tiM#0%rUg^(7RBX1Y2bo`nSkNv0o8 zoaI{e4(eZ(5RzayP>Y};E>cj`KV(iL&{g?riyE(g(_Zxm2t%ckL2s=lD(M0ZH!xqV zL1*ktmh50EiQ1*p^vF|>KL%j5Cds)M()6tGwu-wWYP!)%v!w`xq0Uy{;o6=$y+fAE zH8XejAQJyZ2rJfvk_laWRwse}sAGXCZJD{~j=pE3A!H&>JKsUF>>l%cIbM*UtacYc9`#lh8@ zWEuNi)6p^r@D6ZR>#Fd|46jys~0{zQ*%1P2pRaT?-$QLxX^&+ zX<Jh#MN!$OPnLQLx$ghB|=S0g5v`cYx6 zSr7-!oZTnDBU4pnMHqB#PJ9?s_+F*vya;U_7W55(X7f|GlVOi;cph3|NmrN_!i;aqeN)?y02 zB!6NHFO3~Iu0BT0gd5x=IlBN)?P)&pWLBMEb1&-ReUD zdb-E!q|_2dyfNSnV%rA)7TwsHAT- zv6>Jd18*&UY!R8g37q3hvJco)%(O)%%M1~9@VZ=r2}{lmmvoimDlyl|2bxb3gjad4 zvixBNfAxv%XY7+FLL}x@(%V5^>F6@4~-#c$YUi9v@FFJA>z^`;D zV6jMyE@xqGMF$_VH~1fAf4ZbYdK*S(mEl%ZT|oe~_KVub#3FdSxQ4*c8>%@PkqQH3 zU@gUTZQXd?6#@L`IC69m2%MI?e2EVB2L*?-- z=Oj;UoY#3=wz9X7eT(4NTgzeOyx4hFrhe#iEWZL=D_R(tSh4XS*R97z*} z(|fclwlVsMo3l4s-zV=OhQQ29Pa)>B{whVrT057s|rkRtd;fgw4W$BSi# z5{^O+MeB;tC`kV@%#Su$X{r4cBrMW4FOvEW-`tk*K6k>3A8t*r72fz_dEjJ15e^4=Mj~#tXk^FiA9V-{q8&7cKSt{TN6Li! zj1AldT4N*w0@-r%>$n~wku2h@@~qx0^ccwdO94o8ZR@l|}vC9YmebXyOk zdVem!fCk)u6e#Su=G`JR=}?QotD!ze)WUZ2=0KBlK5U~ZH~e1X>eXka1y>pn-(CUz z{WZX(GQ?mYn}dHz;ql{O-&mSEGx{GHbn~c@oo?hJoHD3#rMzZZPz&t(BK@s>zqD+1 zgXEbT*nK;q3n}Mp#0WmL(4+0*LifQ+b61gBoxpZ|7yH1o9n4PoKW&7*q6Ww>f?;Eq zSRC_zE`R=eFaEbXa|zhS56xf6dP1e+VC+tR_J1HyQ3I}Wp!f!35Q00(KaD&~t#=-5 znub`^r*De*ezl{r7rm=b9m$C8GdWLART}irU`oK?#V835EQTctPD?LC|T$hbs^V41o^|d_(041A<6R4V(>2sHuq@6)i5pVC6{(A&^69$NA{uG zf{(A(hEfer1&HgdrCa*qjaZ-p-O>Zgzc7f?EqRIA**;cG=#B%+NEX9^gv}eoroTv& zxDG)D^Nc7xYU_jI2);yQK?g=;I|*pHyYDd4<}-( zGGE~3E8s3Rla~V_o1~dq&=^I9qRw+b;L81e-zDw}6kxL;fh_uQLpxrD>R(mE+!O*0 z6yZy8+f}N?R@2+JG^Oxp-AToExhyijRnpf}{wS!%3*$F>yEnC`ziRo_O#N4UN z<#EN$QPD=OW7-?p|6y>c$>10ZrVHts<12z(atAoB626pKW~EObd&owZiZlOm zDlgr^S2@fh#VlreM|TbO7J!zyEWx0uWWq`p1F>h_(h$2(y4SP|C~{gLBQTY7H@<>O z=Ve!E=LWp2o!zB)J~m_r6Q!U1X`$KPuZJnpr$*V#%5JT$o_+?FRZKpolmpvIwodQnIQ-ZW2a{jv6)aHT5;TZ; z^xziyz1qJa!ZSz!hP(F8qj;A<3QxCfryth?vbfn~!h_Nfbq!uBad)+oMV!r)ko=IG zvlxx<&aLlPeo%%T0PW!K4J6vd|D_fhuu%L(O531BbJFJ3vP630ITaDsOcdYo`oU{YZY#_&1UA_BFM>XZ_ zB=8cyuP-GGK2XjHJe>C0t%uR{{78Dhsn>lbu*k8@<2B6*d53I@96@6;fOJ+>BAMVpd3ks+QugtOBmTVIlk?;0wW;QlCNXQ-oM^ zY>Z^$H7iH@zaQZQ`YUk30%KzBdzTZM^L^Wrktt> z_3;Idi2?M!2Z_n@7-uk3`&;L0QJg;>e+sXUSfJMnp6U=|tak9~l2AV?Hmsh{!P@@# zcm_Z*s!PdP7b%LQ$#f9zwAtLX3}&d?MnmK(&05(Ko<7U|llnzs#o;7@!xJ3j);`V3 zkvnYUt7b{Z!e@5tO zC-ll!9;q*1l7v$GvfU)NP)$Bt-IPp$gVU4+?hFy_MDUEMfIG(i69Pq+mDGFdiH3X= z6)2x={DX4Ey&FIpJCH#|)MQ^L{;D(^O}oZN9&~0hjR!A-_r>)a>|5$nE@xGh#Uvfpy=DZ6`uRe5--pJdT=udJ@I|r=J z%m~0;U~ga^RIDuYDE7vtp*o1(Y?Yj$Qb7^TJrm+f_55SQd0kY!&E(C@ClTo5U3u8u z_gAoRPQ6F!gn!I92!ZOV1f6>R4)DsGLqVt`M7CW@!r+FmP7P{fG zfGON~JP|Kya3J(D?`771h0Qqi=ip}GS6OAgzPc-Uxr&VD3npb0*I=6DP(`LwH)Esi-SWQ9F87> z${W51eoqt*@gRPOw`*OVnY%ZTPRadx-nrAiue3%dBOPPnNtSq*qC};k1&;qJdB4)< zmXn$ldX=bDA|w&&A-@_ja&-WJ{AqoH#LAB4wXcMaK0qAhbY-FTPUYyN?BWS%yQBgZ zT!h=3Qmn*4Wpz#mMGa|@gdgXmtI48Q{4GMWAX{XzZ|s;nhzX~RQW@6E)rT4c!L&-o z8sh~R7BH6{--q;rd?k>dMCC+Z+CvcR7}ampTPH}mP(4?v?DcwhZo}a-NMQgu44RaO zsY%XQU!u3wt$m~1Y1pG7&iKZ^$PtepgGPYniizho0USGRDY1kh8%~!KCMk_)f2A?k zsHh5ErC#e!SwAh#f!YEKdJlDfRBn^M*IXjyY0j~ol*HgdsMhdjY{~(!Vch-^cmNxI- z+gqxod=k+ygG`Sg$KzWo=C2!0GZ7eJfkNH+S@AeG%j+J-Zj-dd`6P&HnbGxf2wK8sdep*7s_R+rdkh^^E~QqDa&q6cIiT zynL)yxGkAXUq-h+k=2-UQ&TJz{BUKmuAM>_z5TL&F0R>X7T9cg7@rv3z_Nl7;U_RD zW)|K8efByJHpc76$VUauZ^k=g7g&UrfyEeXnj2s3V@t7&y9J?ssdP$CHXRiu&2+Y5 zf=nU)rrX(Itc0s3FBH&R;~9e8i$WW{v7q6ljp^Zzr!+-UIBQ;S$2ScSBu^KXq;9C-q=I#$Oco^<3Ct#%0|bmM#7oSGeWiAr^ zb-Swsnv=Grw1!4YRwYvCt=P!Qm3Loa{+u&m_%VeUd=NcdSb4%0xE+%3xw||tjN!lX zb>w-2%6}1ynhE?qrg%uYQ=z#!y)?C|=ci-wT~F zF7t)Ud8=0Z;97n?0l<_~k@1$3o_ zK=%p`g67Tfe$ z&^vu%@cDce|2FN+?&#rKLo*|Sp0vvrDqBs#|7{0;R5tYJ4m$&n6Mc1iT$|4M+ab*f zzUSagZ~wHZ!7T3bb}xP9BkZclW0%C@DPoxi{lrpZ59*6&j^ihr=}s~{j@{TmFe9b@ zpMA`!%HzjWtg?iggUX>!)Y9)mZ40vlv*A|k3_&3cUs;m1p?w{1R`wefOr;6*I$%Ho z=ug&zOU|u!D|v$7hYoc?zgWrb;%|powP}FBc&k%o+JzSR>43PeGZaXTsLg0{<65%{ z8{0>N+jIrfkfP+(8ZsL;)1&t}M?`}cIr$@>YljOgNzYEV>MQ7WEUD^0&4 zHS26r6)A6GSW+j(=kbm89>jsXAOkng(eps#CA8g}9cm00Hg_A+(x~D`|KvHR`Xk?K zV@IRUQ`Pm4#tw180Z^+f7V0foLoq^UZ#z(%y!0kVo}-K$@;m-f`hv13enG2_G2sn6 z4-JjcboJN67AbR348kgOGikU$bz?GPri{x{2Z>u;`s4Iy5_+QAoyqFI5kQe@o)%m3 zeBw{+Q7s3Jcx2Eo%O|CNKGD8;nsz*&j7xvR69$XfV-K?oh1Y%^%?*rnz*rn|U@-w| zi+RW`R#ZJ0s&Ur|)PALOEDqaFx9;-kCDsjke&hKCd^JFa{X5*C^;x}RpEf-AUZ4Z1 zUvR!#DLN{q>x<|&ubZ`ywE&U_(sIpOxPd4F_?7$RYtpQ--cMi80u z787WX$@&)l8AY4qUaU0+cSkEwBv#Gp6Je z%20lS?8{YVNs<+p2<8BSt%+LD%rvf^2^P{oLG@E*VVZvU#!l$HG)xDG%;sP63G>4ct zB8?d6sz+_LTL_+4do;SlBe-pEkdIuF?zYpJ2mQso%5{d-st5!ullk|?aH zxmUX;Y-?F-C(%cpj5?B^THbNfY&v!O@{kSgH&tsxzU>4KxV+Ai$~#G;#X(DoL!_;$Sor3{ zi?k}cdEFIs)sg2RmLa|RJrGNoSH%gR8xOZ0=xPMvC}`s)Y+rSD5-fUVM4{)4 zUBCIvvlArRZj9RGeQM_@012|y@Mu+_JqWS=SH$}Wz!NxHYXX=clqBef$QVbcBe zkAbxuy$DJE38_RKsEi-3F;_LDP(e$%d+(`-`z?ShJ2N^5?K1j{PQQ~dyV0OV>6BIc z`UeC6-}>rtu5gTO>pRzgTEDkzbw}83vr8j<_}z1lfNV@^u9K%hdkC#ELeZ55wQ4vW zAZ3<%5YNLfp!0a`>SE?Yh)YXdKWmYGpyA1KN6AahM{9GaTDLcUh32cm5Giq?Df|GJ z#U^?BVfw9nSN$gx|3mh`!1%l&DS@x5-F&0ktV|I?PNq^fMG^GUhF$0UhGMfDyo|+F9@V!plk&S3WF8P*-^@pI9pLkS3ISGEMDtyXxG7@hgX`e=QBe+AWjiDuXK)#c0M5J!0DiX!0Zp({#<29Bi2d&SBL+!}Kql-I8QWE)F_ zSzxkVkcqwnn3#y|h?!ib_8#7q5ZX=%@35OGBTc{4sN z+@tUkF7vQ1T)E-En*QDq#PFMC%c;{~>?!!FhqROF!Q6JUzUy{VYck?rJ0?I-^V=%< z*Wza(K&%#Y$oiov1#}R0saJmzCVysWjK;A3%97sB8~aCNvc8iYaGP7Cx7-JRmms%= zcq-8fIVF5v$vFadKkNklqnDQDdXu&}^TMk1L9-YtrmX|Fvx6`PSVrjGEY$QvL(_z1 z$F&_iUxhh~58dn((oMJFs5H7hf%5O{;e8v{DJe#%NfF*PPgN~KAS5HZ@AZec zEHI`FLil%HWh##AmL8lQmDYx){WpdL4Y9|jsRkP1RxQ$p%bQe6DK zv<4T__eM~(8&ynA2%R((+`x#ynqO9mA^HhX7!#}#NEussyjOWX)~vgvytJ26S!(m} zYVn>=Ail(7jQ_g5K2zYt&Es{zHKQ7GY@Hsxd#p+PjQenEHtjm0`~V$3rlWE1M;+Hv z6I{WVEvp2f1Ezz3V&7i+Fni<$4u(?^(XR!OGDK^fT=}Pjsb5!+`v#QhRkbu1yTE9CeWFL*8GhY=E9r(9|vAo7Yz|0_H!~^3<8A| zuUBu!iwY*A#zx4NP_d1G7Z`ev4N5u>qy69(7C=3XXKJIc!X*7ul@((jPZ zB6rPSM$_k!7TPu|+5g)L`O` z3$i^6_Swh5On3wd2z+IkfoCA-i#f3jJ#ANzFlU9dJLY9NNK|TTCP)nlA!KZRT|D`edNW~i5%_34CP5<$H=bvL;cNJQqWq5 zlKNi0vCo7$ai0hK5?C5vF_nPSM-3zW7yEtQoY_pHls`>EV=$G6+R;roTwA4X8r56a z>k{R_)q!>iUu2b~^hstEH=6tRFl};kWLof2-}{=k%e-M!J<`^08#WG;K{2WY&YSmn zsiWy{r|_YXx&tOW5KPU@&+m^aTkSpV#W;_W49&$fDN^Sx1WfgJ8RYE0+%EaPo$TBj zF{43j9v_6e%&+%FWRpfW`K9-^d)7zJUYML_=TUfaBq4A0fB}tZjnWw=wPHWT&23>B z)sNV?=5spG3u9T~XK?b|Ze5S8UvSR_a>N9+?NdpoWGWizh1VSCoR+t`H00d>N{N1;+-Kx{CKP4uFKKEZ;qpl|4gb&53ecrG$I4ySS@Gb8jHP33gya1FUe zWQ-w$RNZjZS8dK{Bo1ZWYhqz|*&`LR$2y5A5^(HUOMH5GMH-E(AfsDesp$feOHm5{ ztafuUBJ-s3z9Yx=hNQDA1{%V~!LO_s@%lGlAs*sfxy2)7ZeQDg%oya}O-(s%{(pSE zWmKGDlqOiXySoN=g1bxbP`CsS?iSoFI0X0L4#C|62@>4ho#4J7>F(K??VkNvhjZ#z zao>9%Et20z{Gir)Ul~^wX$LdJAJ&C&-?j7(1cU}{^U>gTLey2KfRMRI>~e@ZrN?4tI=`yekyS)U7)$GC`2sT-whn%o^4PEh&TmYL&=AuPKOnQRWHf}ErvcFvcI`q67n)yxJaL?`{&_Tzc$r3k|%zqUj?j!q!6 zUJOS|^}L7qhCPo%3oHN}-YV}B>Qeknm(wbN_6iBA|3DPn>XFhoVY zO_S4)PnoaqSfPVCp@)T4#S0BZ;4cYV@d+lzf@BE-$)Y1H$fL#j7P@a;Tr;Op&f=Lp#LttRVauv21Er^>qug0^XGftYRXeXJbW-Br<-x<+;wtxP+(?%y3 zG9%Cg4M~INm@grdAYFcWCn=Eg178`Z(drj%np4PQtaCmxrLdA@3S@ja5FvECuTc5d zBkAMts97~kul)UIjKYX2Jwk=U63lBvzDMs3GFsJt9JUK~SD+8naEXGJ&nPWa6g44` z)6KKFh!XQlVAc%Y!GJbA3T1l+v4R~tAWT;?$T)~x&lqzsM0Lnk1?9e=b(O>rOiJy) zL%*^unIX%ZE8DZh-^k-zhsQ|&wSrI5A*GB+ra{eP-L;kMe&P(n!3I1G@4nWd(pCgm zq`#}2-pUPLl^kwNyCL*Md&PoyN?Pw1WIu5qFk$P)YefejRM?h&t-I%U-Jyx-=*)+u zJ@!pHi#@#b6Y`)NNDs6<6O+xHrl0=u{3uIT__d^YnjTK7pCVUuU}_A;U)%VFJ28%E z4qYW^=j%8}UX!2vnVFjPwnobOhnjHo8;mkJQKa`m&MMeJoe7*G5{;6Hy`3wWva#6ajlLDY-~&xQV-p%q<&5e>WdDM=S3 zHupg?AN6ij6D*j7?}(*MT(L-b{6GtSZM+{7Fl4_O$$=iPWGgZwix5Q#U_&U}8j3xm z1DZd-+$>o1Df|9SOxIVE725jB`DySgK@IS_iZ8x(Ja2W-B1X)s@Byy4D(Ixtup^K1ah09eE&qb zMSKDe0im0JPtyWI`dRE|L}ziNY};Ra@e(6LKuom_YS*8L(`guqL+$SM>@>e~3^_JZ z`|}}$oDwpf2N_NyZyiny-YqevJio)p{yAU_x|kE~4=!ljq8|1B^iCWUJNF79yjoV5 zmUZ!8S^yO_PNFJXGDukka%t*^A}iBSCjU9aFi{Gc7%s)R$+R%`{6%K3x^8mDHlLZKo_aG z{^&z^KA%*|78O5Ptb{s|-DzN!PzHJz(f*X19+haFwQkE?><(Lex=EE(tb~@1FN}N<eHdIX)Wd??^iT z2yjQFxKpt2wyS}l-Jp>M>t|F@@esJzj=M_)Z!7a}tU#G==7hZG4>if4Zw|k)K?V%T z?Z1mUI;vr#a3!_fx6W|6-YsWd6HwoQi>u1Z>R8qaK-gH}sV>hSe3t=0dw>rff|m--SCb#az+kx=l#twGY7CF+H$>s= zn&jKj67SncjdnKtuK90f=3)SXx8%M5pR`)Y0AN7-!hn3=V-pWX(DsCrxc!*1vo&0{ zu5Q}w-Yw5`=pCR1`IkG|&maVTi~F@CJLJB&gKvypIZc z-?zTq`g3$t+XB>+90YYi%2hf3RUEPG!{ND{l+@5YHN^@ILxnSDVYVl4o%4%-yU9u* zXvhiR$e5`8l{FJTtF?CWSFNl!`*VC=`d$G0*UIc@iMoob>W`+)C!FB#eoP5#(IdUX zYED%uq|_eLU`&&I1~?cTdj1w3UydV84aO>C^miCssXv!fyQgcEmCFaz$YKcU8?!El zY14&3q8}OE&W&7WE~mmMi$b%?m1-NHOi#YdtfF9f0q{9ur0fquDN$|PQQgE)2Ge!M zWj2T}d);67)2H>89>466iw76X{NVd3O9-oCXq1tt+`>$HM@F0NWX*`sa?o&4-V?dR zxgkdn^pqbPz&*~qyTjZD8GX<&#*HYko>h!d)1xfeRsb~>xm-5G);XDSE$3FFKzof1 zj+G8qMetfpFFDQ{kR&$MQpR3EB@YDYKTiCn6VpOr4y2PsO<{pjVLfXfX=3@ZMCt z=r6fMt&X~{?5xgZKTstaduQAY99*78N}3`>RUhBJ0KY@=1Cv-G*esg)u^bUHCEg8a zttPuHP)l{SGTjs*>>hZIhh~vu_*=4W*>kZ{79!882i0*+A1cum&kAW|l zTN-JLo;xqO6c)>8)27z-*J=WvGMnclrj2DB4b#{-{(Z*Dr`1N~D>=58a{TrfdCyGl z)>tGvNxP~oJdca0#jD$?k1NCdPNH{g=uZt=47bwn(q_uAtG4JIoT|@;O6U;x|cRFu0#NM&0B29x?i7|J)reE#8Bt__)V1kGc*|SOycF~@B zwP$H7nbkF)ue<49VCGgNZMm>ZnT{i7Rb**!3EtnGkfb(M2ATrrj!W+O^6PXZl_^GD z-t4Mm$NOr34thjYTV6dRz)emuiiF7eLpPY^ymH>{Pete*dg4Ni8p>8x&7OR!?-8?g zU_OYk+RdF(W1D|ANO<@`j@*M3J%-$WZul)Y=oNRc*eYlJdG%FSRrRy+vUrRQ6@Cz? z+cVwnw-ZU!T$f?yyh&W@xbZL1X_Rn6Nbe6bqaX4sn!s!P#vSl&TT-Pz939IYXSQBk z$6ntM&Ntw+) z&!NrRP@v8h7LulojI{8x-ua7kc=7ki!B?<;#d5Wd!gz z50-ZuNtaf16$4eNyVJ)C|)EHE?G@sIsJNgo!-m(FqSmZ?P!s?gW}Fe-Av7_Ir?pdk6Un^H!wQ z1**#wr>ubh&TU3z#D8n#`KjoeYJ}vGx+CfTM&Ap>bU)Xlil9J*QLB9yc}@c_own-g zjuciUX2JQ(Ai^M;Zh+=p$S-y*fRkjl6sEPY0Uf>Q?#8#|z+9#9-Q@cay&D=zK~j(b zb$?X6EPCT2g*}ebLSIC%lXxKp6X0v_l8vb?i?$V7FMnMG-l2V~VFTz81e?rY7h?un zhvWyX%5|;18p^79+U^X{#{0ba@`>-KF80Yxc)#3SRnWsqhIvP6P^lHbXzo|-42z8$ z6t#rr%s~(cvB()udv8Y!PQLa9x0s2L@;xjhu>|Oq8jV90>%w~gUukotZpv#5s0{*r zas%ywVH)O}?z6XErmUv)mxM{;d?~$q`%~@o_qH2#%1=KoyQ0CRI04_zn#R^0FQW=Xa(!py z6qygl`L`MBaW?MWD}=J1$d_=<&z4k;hvw_6Bf&ulbns&53Sx{gNX{tJS!aAlw}Fyq zH7Zl1&puozop$bU-2l$8ZrY{Ae8!Zc*Rl5qo6Y!zjYUCx&jAZKZ{CxmpFzsH#8{aI zC+IBWs)&=l|5Nm(*4m;AK3o%wGN|~O)JpMQ&&`n9Mg=7=h)bw%I4LjV$n*@NTjCkw z73inq%7a6ja)dI||6bnUvKysp!^&$#llX~ahW*j|aDJ_;^ROj?RX0FJus5Y>&G9xD z`EwG2sbPe_han9jB)i2Jar1hhnCiX~mW_7aj&sf<@v^ICND8~twrqSHzvtg=irXN$ z#uP(eKJO#lwX*9p9(z(^&+T}kukp~jo;n|(SMA~9adcl^s&T^!5dD9?4OU*`BelR6 zefzl1+BKf8`7p9he4NQwS<))KCJm)Qgg((+uw%vEpAiGHb1vbYIb__-g`I@dB_6pE%#F(eG z;PVCb#deCwN(~>PsE-5mBlW&Jby>BUzrmdu`>L)pNRJJ2zBnPNWiY;<|Kyt*MPWoe zZ5k*84;%X*IWZjSCrA$O7{mrJ$fISyXWy+$ERYw@M8NT9ZfC55dO1#_WfOUt^^zgP z_f}(W)yOy$S5koC4}zH7oO{}`#p6L7t6yy#qD#itaKAn~8RR02L~p>cH_L&P+P~8g z(wrI1owO_eRlVcrZgyj$639dz1M(Lsi>Rt~9QWCRGX2$eQ<4 z#o=mOn+<<_f&m?4yRJZR9T4{Llx!}Ya*ZD$&>hi09H3t%;lW2h!DXCtYRruCRnF=Q zkuovi<6ov1Q%7h2DKJE}F4hJzmiC|~jEK9SpkG!BG4ZGc-z)rS_8xn&xppU$deOA*ClHrXB0)2FpdYjf%zZvK3t%W#bg^c$c4a121Y z`3(Q&>CR&!vLRN|Zgru5JKbxXcGkxk6_SKbo3}#sAGTseSRN`2I&^+JPAT&AY=ohO z<^mzOCc@6&?ST$M+&6S{O& zQp6l|Er)509Js3)?TkA%<2n6Xve#!uyg}FMZ_~Z7;d)gbXwdG~p`Ma@8PQvR8(ldm z!wpk0_G!T5-uzW3qBDPa;P;WC+cioe-pkd&3&^lH32w#rBG}RU{U)0+wiH)~QRYc` zMnKOVW9TkSm|IGgwULv}l>Kq<4>>qMPqkHEAAD_7W7yLedxgVt7pw5;L2ZYQWR-qL zD6d_9CuEZtbXQlD!IiJ>5-X*)+5&x^wK$KOqY3nqNh3bx$v0MXiy2OQMb5!u*6kT4 zmGXCH>~ruFO&^g{s(A4a#sc?w)D^~y!sUD)LMR}^+4~~3vo%J?9eSA2RG&g=%-j|R zdI<{l$-^8V1|lz%OK5ng$mV1RLH3OMiu+2pPLSHVm0~ClWCr=uOPp4}Kb+v>M3OxZ zh}<0tKRM66T84Vc3vdI;Jlns5_-gTfO&6PS_@OK!Q4e%zF?gDUeV9hPs!aFDBTMSU zXnm=6^S0rhZK!Mm85Q}B`6+X9Bv0Nlr{_5QyrtG)v>32fI$WVp>b-aPcu&2$(_o$W z2DI^*N;Npmi=6V`aDiMHu(~n!aX`eQ47AyB9jbEY0egR@%HT|LwU}D1ZYgOPk%*T! zI~`{zRDF&DV&F|AU2LN>^DVO;~#wC%HpGMWLN4(7EFY4>nuOmZE-vS z5_;&!SFniB0ywIGoPKZXHXMc#_Fs3NglxEJ#7Bz@jGzmqMlq1HS}WgA2AjTLRU%3o4+{&$DfPrIb;Jxv*R{_A$4axb8OS#R0P#tPH=^G&X4Wra&P@~#c= zafpoRw#Y|_3PCc1ZGu)Lux!r+PIE_-0$XW#dDG;5ES@yS1-0AfFt?{0js`c9=dh-Cx}1RNu34diJG26nK4di9CbPLAHFls;T(s#S!uuk_6JsqVfLP=Xc0X~2 zunp>5S>_8rv_~R1H;c$Ni+_1R4Ne@fxVUrKZ96h_$6Fo{+ z)0A6tIb*LL(Dn@hS#UbufAV|Mxyn`M8ZF+Mx8@#nWnziD^|@G%R^2X(h_#WYV{ywU z!WvS}sSzRrC3WA;Sk(!vXuW5Q^qFf^hWOr-irK5JvxseN-^6uqV4B!9;c~g%+z3V8 z#sX?qRwN1|H`aG@z+P(T^-b>R6q^|xmnK*%tFAU5@#>_)>Um=xPFu#EVPt3TAt4h$%G>!;QJwyqvrMMO<+jd@99}idTO#%lc-=)AJ zwpGITUxR9d@E|6xeoxF;8?)qgex?*E`@)N(STFLm1%L(!<6GnE&9NZx(7zT2uh^W9 z=#lBb15pZFZ*WDd(YcTJ{H#_kr5XO*93FFc<7xL9u64k~dh>}2LuaKo(P1+g8OBH} z$ie(i5K$XW3F9XEY$M%`ijM_8@fG(MHVrOCmJz(RmX_lN(Vvb>bN=UnhaFtU&fhnL z{i2I9T&;G1j-cz8Ejp{_Q9KFnlj+r-o0smKb+1R?`dKihjY$q9(M~ron%&a3P%W}O zX{Zy2DduWShcWMdTL@GAtXnSn-_9+?q4t|aYlp+q;@+qGr$b1Gm;1fG0X#!Jo-jbJ znpFJNMV|89P}<*)@J@LC9ovB;XkMc>>h&+=whLweKuZBZzWOb;la|1iyzUeJ=2SxC z(P#M|6FuH$^eB3>-_7rFN`#PayOm!Hiq~8O9;U@LL+GilBBV>LwF0nNQ|hA*9}UiiuEg1|I4Aum<^Zo zxGLT2DAn)h5-{{NRe$Ln&Lcx**DXT#915K+hc+LKq~YXO1sV0@r|rx_;q0|J2)ae=DzVZ=Z}}tm^`8VVFO# zs81mzH3MyStB*|4pt|K7;k$MBdz#%6>*iHfhn>{^Y8l~d7%XFpqYQrny+81}@p3q?)^?8G!G=~tD470mY3)Tk{DgEjYWgc<#$`9S zW>L{XSLoo znM?XcB=DqyDlOIhA2rYxZJet9ZnUJ^!OqlDiLHwiq~b4rB$=0cS7(0v2q6|h_PO=` znl6TW`9n1jXmY7Kh&IzC#va*LsV;<%C)4t!ry>nl8zE*|ZuoY!JY07oEne^UM5208 zd>;?5THFm65Y`H#Z)yK?_?qpcRQpsiK*CYxUDfpIT#g%h?%?E|)_FL-o4W^Qa46a9 z-O$9;$F`a9t`C7HkC%eq%6+@2&~*m5;gghqgcw46n%bo861tkmX5qSUbC_W9F)>)Q zeJvG$>b@L!aWY(XA1G5a#E8Y&y%W-F-=LVV0gwNVsxL<|T?w21!QK()>;=Q3tAN2Zc*dhntG1?o)ewc3k&nTG*PlA&G7@_ zBMiKPcL~7eq)%R_V;btU%{ac?3i)w5qJCuJ_#70w*={i)#hlf`8sq00d?E&wH^UH? zJ~e8FMoQk#uB{#CCZM`p!k_Zm@pG={XV1N5KsvVtc|IN-oXD!lBej5xYZTTdZqz0K zu=@lX&d0)dk!olPi3n9$IoDsqb4@X*cKuac!^IFRi5!KQ2Z?#F4XA1vbQupQMQR!| zHI<(bbh`tODxC>|Nh0L!BtVNM3Ra`V+Zh+CLkhAnAj~&Z3P1!P8UL-1G{LK&>y&5y z_O(QcQkR)l6nm#Ceh;3GqD$kDA=;SIb+z$Uf;z^Ydh2*fMHmj8GT_{xc79%^VT~O^ z)3X(rp`e1t9vT`5ik~G3N0~kykSM)t{A*jXC9It?K9kisT8d>ftq{F-!H_5rx zyLf{WX?=$L!mDAMU35U8TEcFJgl*uR0k14YP19Wra>yo))A_Rix8;lV3(8A|h&fA?%cz$dvG_~z`w7JCAWA|k12guS&x1$x;cCeu7 z&GCZ6NyA+4H(fiv<6ikfOg)c{5Qhb`p!^Yu0siJQG~aV5-rHq){XY7SMex^)S#FD^ zcBnv1rhC?hN!@=?10aJvdoy>QWpyMmzIg>!m)T)kp@P*e@>L$H$O<#Q~~kJBNLs0B^{ z9nK38UhRLLZ4}*bym`U?G@36&0Fpsd9{yux)h&|*3((}UGRiLJ6-5^;skVPsnOGk^ z+xOg|-FYTOpPU(xp&;BPv7indiip<5+$`MhG4b!0FOpw|t`=u`{6HzFdKYx%%$&v7 za1xF2UCjbBp$Q!wv!MZ#lmHm3nkz<=`=_IrsnEmt5!I!qO7Rr~ju161XQpkkE+h>w$&Nzv%;Qf%p zbk`}M&rmJZxza60BWa}N5U609aKm(Ru+#Y=>&!z|3(Y8ZxvYT)M6f5gKSQsTAzp2P z*s`O)kSYK=KjCrlrfy*zY!w=|yw?Jbg*s|f{!rLLXg9HkCR@hd zuCC_a*fWm#8nif;4-Xo-rD@ z#e05SKi&)0)Q5SSp1O9@R6RGrSW5nYG=Nb0E50g^2BwX!IK2Syvn+s@RI5cXW~2Ty z%fFLU<@dUm<9a69rj!XyMXU6HM?Kv=t2PBR%9(gNM04hc?8SGqkwljeTL2nmu-dT#i=%*+l^_ zw>U^BIgsxxZZSVg2nRh4`S*O)M>clHX8RWNZotDyO|F5hUrqNu&&ws$1g=&`6y=9B zjSERslwicB0qJCLwoB+m#EhQ6BUrSe0}pdEqV$LAZ712U8j%tfR6NcbTors`myq-F zbfbO-{62OLfXC;RiH1jztSX*(as0UrE{N`_ATIB;M zrn&4DzYv)xX5dD$;8D)EHCjM7eMi2$(;B-ELR50Y zg>X`ZAV<6DgMJqfhr}S3X+1N#um+P~?{i69Kqv)zg;cn5x~OsUwk6JRucM{NRXN3R zmh^^&HMX-y@n((HMA?$d!5`n#Lc`1|AWUQA-*Bd|$dZBP3e|;nosI=z4)r#{zcd0t zG+@xxK*>DpS&F2G_XwNa#&wI*jV~yZ1<~tK_!e!ST=HugFsqIAP~?vd#p#CkmF zydwRW6icc{K&zXz1`oiy%y;+*>v3!zWpgUVyWkva)qQyuBBJa=DL{D6zhu0=JLIXp zj2=QL@(^_CT+S4=)ELT)6&iX!hCB+p_f)}wY4JNw$Tt4U1+5JM_JK>udD*ar$gpMa z%hCJ^&D5K_t=K*>I1=ciS@7Xlru(XA_eHW~{tai$-D7VE(gyJ7us1_h>+oZpYs zSFIn~*IwvHCcQUF60OVSy)xM@l|h9(g-7KD^Mqyg|8dGcZ~1AkvkS90Hq{v4XR8m2 zfHvXDA4thb1x2A(us|uFTWjt0{8lory~tBlNdDt?eGaa3&_Md9TFXI)rW28yXsGmp z2uS|&T7mn)WD7I@$yB|uW6wXIgjT<9^OqgSrMSoSykHM!$%Uj`X#>sw?ckkN`+Te{WtIR$r6-<;Cy_DA8joYF>w$T?h;Tm6 z)oOh^fiouVIeXtHNL?0f{)`lJiID%dj^SIc%o|ZZ93cmo3+B-P&m*XYYsGYCT*q0{ zTm;r^+w?ng2~-)eO=`q1P$$ay)4{iNiq9{L)(Oz%niArC zEjZQ>(m@<&XRJchb~P{rXIB3ig;-bo*IaW?=R2c+DfVu;2Wq^0_*5YY`ggA4tA3!* zGS&GlN&?x|!|yFfM4{fVvXbtPv{I_(f0MH$H;?`0C)8|57oWFi>fq)!a|-mnlY<>& zW+`X}OIly{nyn4)7O+U^I~%I5 ze#qA_Vgkx-s_fiJUvLBlaVmJV9v$`1Bn{qXN1cJPy*T^kaJc=>~4Y&Ny8vj8=7`GU{ zKiH{*OH)*+{|h;rhx=r1-m5P^zKlKR(&x*JZQ3{DehCIpY`!v}1zrn=Q?|(NF@sxH zi>5ab8ygB|=P@Z2DoIXt^W8N& zkDx5)R~Qtp8%{+Y<)h#}n57qh#5SmxI`>OA%AOp#Nf%*Wk!N;swn_EFD_vRBTY5nx zf0o78BG1Ie)x|)n?*GPvc3KTov)?8szpCDJf4*%+6#U32#U($01aZLxd51`WQoaf4yFeEv*? z@p|FFk-qzInAsV~il;rFh6Hr7Xw$n9)H_o2g{9cuWDyjf7}UqDy1nzZn3QVp`w(cW z;J`C!)HfjEw&vq5=DT8%<0J05riE=YTcPc+;yTx}n{i21I%(i@4dw-ou?_dG@^-as zzn6Q5ivc_d4(-Y*9^Pa?zZIzO_P^5MMTg!r2Qd;1DE|!q0q)Rm0rE1RlBNHz87HR_ z>j4GkW7NTIp?{Yp7BANYc4a7?ZQ=y`zoS>t*{8=RJ7nA^H=RU;_v6g8bg19t^4MVx z>;6iy&rx8=q%_~2X;)vKHgXNqz z-VzVgt}zwO3SHI}MLqXVr)kVN&!e*QqCKVv?e7!3xSsC%kzWkeO6AX8lnzsLL=^4g z$r%pqyHlpwKO2{kinvY-8It+cYt0WGyEkWK+LocJ{K zaAV=Qzsp_v_@yZS>W{QF^&(2im;s=J3|f$7w}l(7qIAT@^3mac)>ClC0e`c~$D5qd z;lCWnt^DLyBYT`~Vtca12-21Xk0T>9k&&0!3g+w^Wwl zYKXpdc;L5sNMOTB{r%yC0-8z6*urKiQ93ZmxW@8MQy;NvTOONs;Y@HUw*D93p4o&$ z=_{c~lrj^a9B=;l;I|J(itZ69_2Gk`pyHIG*hI|I)AmQ2s?&b%a0j&!HmwdSjmufebL-@^7w_UtYU z%pFM}mc?tnv9aE=Fvb{NGX6ga^x9?4z! zAj3CsnQ`6?JXDqu)9f@-ZTDa$t1ICK2>k{WS_zI-BFOq1bRSu0oB+tjLwq!*%FN5L zOD^zi6#Cp2dfi^u&~k=|3{3Llfkg&XO$F+r8r^+`RY=X!&}9O+T(Gf<5F}LYLRoex zt~U~lGeAR-e;1Il$Y=d5Exk@qe1EnUs4%p)lWu3QgLR4@rryA-OS~jl)J(Z_2 zlJ2Uvwl2voG*K6cy;S+r=KL7T6N>iLCmVIw=|Q1V1)@$~9Znu7_PXxEU<+$Ys++c2 zvR|K!mubD33~AA>NT*E2J(eoPT$4^JAqtnvqffVZ4J&X}Ds0+5f1?whGX#Xd-e2WH z_Ipy5213iDel0CTpxbux;C0_U!5H>*$;h(8Rf6ly0QH6|b7I z`s4Kri_&gSUy%IRpI!9ZA`T{uEd=Emom6I3dUb!jky~a`zf0hSf{>z!F1cv(9^)rK(5e)g-?eJxYZD@a+oC4MlUBchBxgY@-o{^_f@$FOy%ZXHEHVOU$SC z)W%-LZZe20*1h8UXeAW;UslDwzhazDFa zPcqiZ4Q!y4zY3v>laMHNqD%4@o)xd(){al3fYh}UF$!Nvl(zWlH*5ZU{$BRS6<2#< z4OMXeyTh`*Th`-F014km1bHlJx<@E#p`yDQ5~;Y~l`AtC6wjHVtzaVu`W)SqIpqR-FOiK3$4<1@o6@T{q42NL4L7u4I+o-I{X&G{7wSX> z5qSUG&#jIg(+xb-qn&z(3^e!Wd%C6y4$JUv0L^FJ^2m6*ZBDps z*S0qc^)U*Kqa%mfOe49qyLF;tbq8;Rpc~b%Lk7AlHlUO_x?kSL#-&@Ca{KQo;vPC9 zqT(@W7Di-|V|umm@KzJF|Ks!zM>@(cU|m2ruofT=g$5y-fUc}i7;T8c6`K?M3&EuU znF+f2jqMn`ux)1Vl;@_Uvg^H_rW@|;jD)3?b~ff&V{O`MxqAUBDR#2{HpJDBSC^R~ zJO;~@g|eF(g>USlFjN2$R6YsJL@WO2Dk5}d$H6)9i15kBuH)% zZuC;c`Y)Hj6j|Ej(;g*oUcFSc`4#U5!^%*$QK>%og3`!sk4n*biC@p3I@ z2a(DczaX|e_eiFV#T$BXLbyne4*C0h^uPn2E;(%Bx+SX4I^Cu5mP7Hif6&l{YlF$P zz3d(R3>$2+GiCl^PWc$;?X%sSU&T+97#cws6}mY%b<_Rj1Lr*%4c*xNgyR=d>D=VN zlQ(37XU&^a@B(@*Qm1L5-U2Fi!LF@!N#Q6!;>zRkq-n!K!9$fAY0YNla zo=d{0@m6PQy;?_mU#h<2Z?D8N_I-(kFo=zjuElUs9`jTnYMbYpEk z!WMp{5xy+mlC9bB%FsE;#2OfEOuS+Z0)D><((PXC=P#D4_j!xpNU>bfY91sMd4i-2 z@>3ymYzEHOh^ zh>3dqiYetelJtiotxLK#$2~J1M2e??$f0fnICI6`_0n+zgL=Kj8c@gN(76#l&f=gP zp7SlLuqh~<7`fDanE-MhI^p0A@#Zv;DeqyA-0MDA)e;+8wvY%C4Z&`HjE@{d*$J}N zMFaH)h@NYTVlXZyZw;gyo#M^dYgA!F&-dJji_rNPMWu0NRCnGHk9|zk&tXl9d|DY@ zyUjz$1usbr5$psE4b*W%1P}R>Grwf)Tc^hx70$#o^hWM@B^00Q;T#NvJ5R4>RslU@*gUwGsn0W77B2u|x5*+$(g^*R@o!F|kb4^Dg%`TZc)m0uK5uK=ci^ACtGzgcjv82D}u8Ff_mP3};iwh2|YD zHRfZG=$Nm@ruloQJTU|#q)#P>mvx1;Hvxsd1GDn%m*PxD`w&JYW|RUGQ-;m>1o zD@!8q`px~H zBu|cB7Vz)$<8JXrF_x8+ex;{b7gb8aaf2@gvVpg=8`6KdRISr$^ax zU#;CG2hmD*MUW~`1{3jG(%NChSI_=39n~*GP&(XS6$!a01d{=pmB;=)t2vRSuAl)8X&KbM@8Ni6{}|j&se_P8#opUWqiyGLmoJ0nOS+ z6P`SV&V}%S@Cn*6U4K6M6=b?BDNg1DueOBP$3x*D3+s%C`M1L^cx34ll?qLBYIuA&C{i(vTq@f; zbVd^fvFds+Xb@s#<&oPdgZ5x{1zvMo+~Kf#wv-r$I+QU!OVyzC_97G&XSoqxU0(;} z)BVGQZJSnLd6pd{qMt^dTgGuT^&m2AdasG|c{JQSuV*xk{^UyqR%DE#8o&wOUe6h# zc^%iveC*@Md(Xe>6fC*k5h5z1Zety1vvkR7Ki%l3j*8y`f3vVlUzPti%hLHQhM2X4 zQ16V_1n2y&t*9)rO-sN26qLyk5)~$82Se+XL^dU+QT7g+NN_!0QV#OYRh}m{+9oN8 zN6C#I?|SphgnWm%qNHb#LHe8fH)Uy+yCr$_L#x9$3V3c+pWC%j-rasH_v3S*sZx+J zGV7W#<>c#*=@@N=ok}=;WTkn?{A@IFiit!oU;{WNpU-RJ}M zmf|paac=Bc6se$}n$RS5-9|}5&RNImpzUUIpq0Xcc0xgM+##Syi&+bZ5tFJ9JlacD zVz6CB|5Zx?yJPDm0{sObJwP=o`qy(%OAFP3CeM)|VXDnuP=(b!2Dyj)Xxo-UZ?HW0 zTU@15Z7av_e;!G>UFISYPgKHfu>yVy@c{ywr7S z8?GHyvXz$7VV^*JD&$2#{Z3r?)BdSn1X6eCd=V|%uwBL1S zw+;>sn`ldm1%u&{9xxJ@J>Ar4t4n=0yiPG0T!E<20Pr>!fs2h#>@qA5{vZ*mZo@}s zZ~duJGUT@69t$+XDEXG^S}p5bEvu!06+%R|-tj%3PdzEDBzzr*Cgxs5y9&+Uck8I< z8(ZsE^@m<{p_lLsy)oVtX&z6Gs1i`W(>USAgl_d@X%ZzMX#j4m%31CUJ9z#9e+;R*t!*C_)z(f8U+tdt=#wCM>)~s>mFW;j$ zb!&Ktjv(5Ny=Dh*jV8_-zlT9>3vjq7LRTPbMW=QAmvGj(Cc z8sg^GZr$8p*Z#6xP-NQV^^DOuQI&E}ZRaqx=pR~hW0L|On!^G}R7mxa0U7nt&p%|8 zg=S80^_8rMp+LaEZDq{9qw|a?eDP++aX@T2W(|Ry`UJRd_X&xl>8TJ`QHD3)QD%PQ z_!?qlyPajzm|w%J)9G|}h78eqC?V0yWSDDPddICQwNw7W&TOlM+;*C74+dib!eQfK zy|tANOGv@aFuNf{V=I1VarKOum(1Ymq{QDZo5q;l2`nm_9S*Vr!@ZPgKJ^$~SXP3` zLo3f!Veo)Q_5k40D+$Ae3H$d6%Vaj3nT}n4g+cC#?eYu)w=$2z943AjK33i+%es9!y zSBm5N0Fl=R`J)dPvgOIfd`~a*h%4wS8kY`h&4}5)s^lzJd6?am2v=2Cg5F6;lc3%X zokhX-W3X_mQU+H*if!B}oIf*cC)qzeovKZp8ly@YLUjg2w;m{!qEiAMxSu8H7TwD3 z*F3xMwj1Qz0CY!2hN615TrseqbpYX{gt0Z~@C$3%z$D$zy{M2;X{Ji-+rS;2CT@(| z#d3VOpwKfNb8ENbw$8)YBQ3r6cHBsnkQ>OOY`Bsa4dQH16TRt_R%Yk_DI_bHFS}Qd zh_59B=QEI5JrW6`MX}Q=yMt$7)xH9^}T_N(fVHJ^91a{Q1 zUQx1wZh@;PD>dRs%Mc1gbKq#R#nlxz&sbQM(0Q`b$2)wZe$pepHxcru~4F@GPexyGCzC z!Jnt1_eLFscN5!#jH=D?%NG}jfOOf3Fmghyjf3#3@NRnKSJdxM8THom;v2Nyg7wR&HxH0%^hVO!pdrL+pcLfq5*D=Z9UX~w_pdd< zCdt|n$`0O>8A%6NE`Gl3!3P?xmL8Ggd|OPfieB#E-mg@EY0<1X?U#nw`Jx`Z_MPnx;X;#BuOtnrQEe771t4Y{-{ zM;9z0{@|STdk%14e&d3{flolZB;D{83hha4vIzw3)85oTa(Fn@R5v~l8W}4vWck(> z#)qN?^plbIV*xaZrmF4xqci?lJlLOpB1ptux?=X??yc@pjE|J}Rnz0pw;iB;CB<=K z9OA9T8%@xGj5tiPgIbf3gSdun<+^DsMYpaGl^<(z`>LXsm!a*@{(mw8m#e3zOO}tq zI4T(iLBS^AzPk@IGKLtz)h%e46Snk1&c7XA+W3x;$(uPQ_Rh@E9=O>@{eIM+4%Q}n zS>1|@h+zl@Z+)3G$2t1;5%7~_gx9b~Srjf-Dgtp3n~zpYm8rVIi&P;wieSoZpLC!s zzz4W>7xu6a{(99^sz(*d8BJ#d1(o&NEAbC;T$hGd~=IFr68b1JZCV3fnm{Ir6F z_pKn<)ifrs!QaM8;fr?)OaCQ_SNs(jFBpDBZ%G(BPD`%HHqh5WZLXHL)^BlrZIQMD zkNAifvRQ9llGpm;UCIq|yX4GtfHploqiNu$o?xV+@s2jN`CK`ZciX+VRGlUMw+^X$ zGM_)04c#I`8o~cFM!a?E^p%w}Y1csuU#5+BAI>CyA^BLW6hdU*j^Q?+=Z4VKZ?jNm z=CB?BH(R=)0jHS80W&82Xp<`F$?;t+=sJKfdL(%A8{02bAd>?EG7DU53yJS{wgU3c zLkhp|R|G<4x~#Lo?0@hyJ6KEVM$U2~#dBzt+oL!!+-i$5K5p8${7E_S!A(I@)Tf zniLRZl{kvXF6GK`E>w@pW>f~m&dl}72m@xHPjgjY3aoYAA$u`84W$NxYO@%62e~g} zu1Q!1sSNT0l#~wA3?&D$%W@OO%U&v3aA@Ruf(D?BN>zQMV$^kVt< zHxbie1(3IhIRP(kYBHVslsCow=DkFzH{30Ym5oc#Wk)B-Wa?5M5XYR%x4n7*P4Y*x zl~^G|cC8V9ofZ9(|Ao=M0j$q^k$M6D9e%BGqyS`JM038f_Wyn2{zt|9`6~+GvbDH@ z?7@=YQ@Kgfw^;VvtC3HtvK3-0i5&9Ax>Fosi#?sr*BiBs5#U?C$PyL;XcY?=c^KY25cnzn@ZHx!i3@61wbf2no- zr(&_u&B%-hdKnA!1+?M9Z2R&aGhD*O&Gu16v{iml8S{6z=Z?I{eOt_!$AFOSE9LL0E7+Vxtc=#VD5B8+UBZ#m@fqpvY%-uWk#c zm*K*XnPHXJ)#WV5n8;L6%6P2^sa(=WLvYe1aRz`!=W0`%-IUC_+I>ELVP#a*WPxB2 zRAG4WU2k8(f+w9u<`m6bau^Byu5>yP@5&=YMto!kDk;;-e)D+t1+qb_UcV8n)?&V6<(~1X8rP zyF-h+duefZ_fp(Sa0t>;+}$Y}9E!U`DekTXic4|3;oJM%ea?OF{DTC>fI*m9Yufr^ ztB(paMPcgbZMsH$gr#ha#B+Q57-qO^w6Aqux*fWY$1v|7%>l*uu%aM}gw>ZYMI-M) zLIGmrkN1#|1y-ln4-t#4A31y7aKLE)HUh1wf6yH0RV6lZ)b$O63)k5whz~s{DQ2og zQTMm@(KvUuPhZ6*K5xlikBM$Fm$B(NzXEP!L9z`Kvj7B&>E7;4_I( zg8x(dj&jN_ivY+jsYqt-dx~H&LQGkqZ>67#1d>Xc+s6EO3&5xfge4?M2`Lxoa+C z1y4#tUpXSJXG22U`TN?Sejp^7O=TI61RKI9Zwn1azC#M;E4&6LgRbgP%f9{`H`coZ z&eyqwM0KysE$XUUwULRJlnzaU@+2}J3U%x4Y9+PLVJxQl%u#(sz*mnQIi3j~Q5$Bi zZ@#O}^i&?ytdbD936ZD7w<{7^S^4atJ95RvAMJV zCd8i32ESaKgRiKutmyO*k;kS#T#Jw}DcG^QU6SrXr>luRA|ANX1(Y^Kr6Xf&Ns4;7^Yh7 z44twP^BM?SOc$#iW@f{mZiK*>pn2oneyJ&JyMpokC=v^@ z|9^S0;1|*MkKRRO8MIt+cYOSKK_$8db?RfrVMO^dTm48R(?t0}Ouy?rV z9QYo_hR?knHyAME(a7y(S5XwLnKT&3=wNKRd7l=_A>|jx@)8W_*-wv6S6jcG zdaS7STBZYY#vnT(UK3-{0ZyNh4J3tz)Lu!nqkBY%cATgKzdo7Z5aBrR(aCPr`2)2_t78T?GcnMY zwZO`c=wnpFQ=?srg@0TkJed4u%?HcFa&#?4N{mY_NKsVcC$p@`fW06p!%UcKG0Wf#qgmJTWM8?~xgnN}A>Q zIj8+d^V=g@uUR@|oU9nwzl1Q$DBbYBGZy|GBV|2|ycafzvuQJna13&KPA~<{Brbgs zk8mfuCk6=CZGPgeD+-Jk2?LO5mI#!%wb~G7q%44K^Ee;WO13TYuuTs}RcV#3N_k-3 z7BjPmBqcF|zKv!pq>Unto<@=4!bxUN)-d0HMe>fLJ!j+-I7PnqzrKAyGjEBh9i<6m z6tZVw*s70*w2E4hm11(!S2rYnYa4a0{{t62R436r!Z-tuqv)!b;#R+}E{{)kNT?ec ziDMP|bNFF*_k?F9W7=kw9ag}ynMaj3FKvI4n7_c~^>^5`pYLD`$23(X0|gqz#=rk< z3TRz*^Quc3aB3`}sZV)#_1E))kCKHaL!OMO7}Gt{Ww}3GoGI{Vgm3Nj02u?QPQQMD z!k-jfa4#nf?g-ZZhWKK~m7<<-VRm;SMT9kGqQcnI5S8J_`?Y8EaC{bQ&>AesQ}{6m zN29W7hnRmS#hjONp}hk(E?~0juSca7iQ`^tYBc-e!$OPI-qVyf8nAV62U? zJWu^54b`p!qiL0~iz^WBlSSWjAADHWgo-hM?L?=6x|0J?MGRR5bWVV(3|zzSHyfRAi=WWuU=8$@05v*Msna>Ll4TM+ zc-VkHdhTo59rUPHxF9o+e&jOY{J~Sh2>)RR9kAnGtn^}CA&n|&?t4MP@pSy=PRhgDDpq8UNq)v1*+HX*{!4l^wG{Q z{zSUL*|0_qRh9)F`u$tc=?CD9>hs^=--~H(@mu{o4<7UbDAyldtV>E0tfiag*wT8P zd9~XAQJI%m@MpfgkNmWjKd3wHYffjN(J28b7g+6kHU{EjOugP}P(9>%yVuWgIIQ+z zheY*|`-FA@#nar?`4hJgvldAPkQxnD(k}0uH9*Jp5 zNq7;|4Pq6&Ep_xl%Q3Je4F|GN#iiXUko2!i0m~h7vuF=j=NN3N`)DK~=O6=7^4KoC1y?-F9f1X<8iW`$&40{rFkR5~$|C{$%w|;fr>>sh`)?h_pu#+>t zqy6ErxwS|Vi5}MfVlbx(xQ0t&?}zQNEZKhE%nN7ThN@hC;Rtcx0UCFS<_~ilE>fUF zM>nPz$+7Dt zSE!IJ=4J4_DWb&UiVf3KYSmxNY`*c9CKdV4`A@yt5J0=KH~e$Y9WEKu|E`_1BCVW$jbBsv`k3-VrlLe@s_+`J+! zT~aKvv!->e80w%s2@IEp2+-Fgq1B4_x}lVfhhaYdtk;?R6tRy&YfOV z`G`!D>>Mu+fN;g1o#0~Lw9D(zPF`3-od4;czNLP@Ahpr$gbG(|=%>p$T%+6qwA}4Q zC0mV$Z{b$oF)GZR&9oC0o(v>dqO1!(vo8wvsW zgyg9hU+=wUkofBGPt2LwO8?sp8jrof&PxtAHXOMO<`o*YVa!G2{rvgpkN|kb_&i1? z>jab2#%8`f+cl1~Es95$Y#y%3(N(gs0-KdW3&M~~ryzOKzDej`d=*9Ps(NZ|YP=N^ zEF7PWe%eSW#Che;8;?rS>2Y6FQX$8;{AjywN72Lw)H`ZK>aic3;sC3(WHxX4#Qvts zKRJOvrf2#47w@kf9BY*t8#Qd9&iG${vb2xJXz{k2+_D#S8K8bxd(Y+qJM)X6mwgh8 z^vRAjs@p^-F#X4xSI_2p2hj9ean_q5*&G4`r+}199G>0LOw{|sT4h_PJl8ZC|Ky z_cNdbRHwr2I32gEUzPUGxTl%CIl}Pgl=E3#y6J7gL*v`o%tcxQx+Hiws^F$gqc?G$ ziVi~yq_MFFj8p8c8#v#X^G#e9R_APFxYsUVyo0+X`HTwJ2m)SX_EJI!VAdB7Rb-!; zK-l{hJy4hl_u#P5scSMJ(6h=GW}KqGJGh7V$}j6_>jh=R!`ep4qJhRwG`q<7yw*TNb$ zF}+u!L?|HWs8I|rgc(g>9Qg6@$>>a|(c?G_&XRmjr0}+((GOt&&+foW5(z&_NAr*^ z+Ilyg7IVxX8hKw`+VX1zT7zDYj#xL2sKB!#<*(2C-WA|>!39r%M?OCyBue%2mwovI z@xF&jSM9MdKv1B-Dlqs$MSU76#_y!j;(XoWAen9Ly)2`TlKnv61R&uRq_>)W-IXel z*AeM=##%{Man#%_N@blVh4s!mh>4y6DF;4QgowtLd7#fe)j*av=D+;$mXGf)SJp)d zb3f%D=p14Y1o>3&Nqo(gh1o{1Vte`c0va6UPSE-w7n*fVF*E~?l`re4eitkd?QzP} zUL>BEAse}0>RURX>ra*37RmwZGJ7dMsn!sl(MJ5IY(sxe`)l2S;4BSbT*ad9arc`C zIKdPdC$fo<9@}R*y6uZ#vX7-rJJ^HZc>3?_j-Y9txoQG!6n{whlh{=|NFfSpbDy!F zTCor6rl-yuWXuzv)STgpkNJX0X{tIPJmPyu48;J<&rpmFo?9#?hUJejf-v-=}u zKdclvv+A290L!3LnKD>G`}*_T?XHIQrG~cGM;mKUciULcMNGv%RmTkunkxb)o=5u$ z+(Uvt>9%SWH-(r?g1D_Cp!KwO7eWM}r5*0F2GGU?QoZrxzV-&}eF9E5NxF50F%jjZ zB;qta`=PgdK(_0+?b-;2@Y}am!@oH*Tz^;I-f3$|r zl3cjKiLLigsJ9jdAx$9{r>LWf)LT0JcurnkC;GM5F z%Og`V6Vy*qqXj(Fh>C;`2xO4StE9}8XAONfX|H?0lx!b<`5K2=2zoBTq#ad0jmS@T zdz1ULh=^ZD(ozZy`?0M3Q$^!(*RM^B+OGMY>+>3$gt)ssdZF}lecAq6q##?*Os#xB zvbYnGv3n3K#G-=oWjAio9(msuXP!hw`>tq9d`$*er@&hFMwh;vH_=s)=lJyIXW{Kz zspi>7k;2JF>G>`3j>1jX9)-^{yKg)Yr)!BFycp?J7r!2O*x)MUt>peD-UtXQyk*3X zRX|K+c`!L%Zzg{0!77IcE@@md0i2&aG&={fhZtUKol$1PI$<^Q#7)=y`q)Kxju1(B z?=9fCKi*krKB^};*O!{7_d5bjl@53o^;Jf?6yvTKRnL9CUT&x z6}2W`(a3z^Hox+%R;<-jtcORjV!0RXGPZa-6>|G<;FoUZO2TJS zjaQ5PTeg$=vJT$#J%uc`a}{pzT0yi+;I4`QCozc_QlifR(VYyC&by|Lf)#h_9~4PC z!}dYj%1;ejoe3PxquW_h1qPhaAE?Dt`(!~Lgm8*39jWY7$x?U?be@Erk>CC@VrHdG zf9zut$;W+1pgHoa2_KcpcU}8TUd*!03eEP z#S^&NE zF8Eb-{@f4k12mBz&YV^R1KPITisQc$?OBVOn;*m1TRtEFcD{HuNvmEy#(VCrC`WFC z9}Ybzs9XX&HAmbNk$!e(YzJ`;9`Wr4n+s^_heQ&}=$xvR@8HJKN0>(s7dA0ab4)a< zeo*bi2QAU7k^>G@WmZuHT_1 zG&`F2DU%+lK9<9KX;#@AW6lch1q_W6$x3B1%4`%mmt6pdCn=t$GXDb^TL@vu=u4oS z?3Lnx(|^KmhQI{GA1y#^mkC%F=DL>%%Q0?E^d@dDazi z;u+YKf7&(ND6#k_qZI(ZZ86y@ z3uyXM zRb)E(QpYjU9f$%@5VG`Y3^PgTm^!52Sj+@sU_gHxloa0)JctHf^jg@{l`6=4`SaRi zoAz-!UWS6L@vMDXSiF%D^DRx(h`$s);W5?6We_9kIs(O`2hoqeG>eWOj&ZUg0<;#% zdG@^7J#d(qehC@Y)N5m|6YI_dLAkAy1D96OZs7Egkch60j`ojx&bH=0a2${D#qa5F zBj|2W7wubpsKzQ#(y_!v#z^L2DUKoUi_j0r@}RcWwx^%Gu3L6Q#?2pOZ?+HR*YToe zz=|PTY@wcL2YBfB@Q$cXcW1_CqH6wyTN21|wF`n*s-2oQzc4ywMqK+y!rG02r)X8M zBGpGTeIYv2{fQ<D_&utpI0y^W9-MiNI@ zn_?BT5niS?!Dr*?$CGB+CKkxUcZ!csi4;BY;*LeuzQtQ^sC7YN5cxFDA1KfE)(v<3$2}A8l}*MUjP1x3G>E( zYiF7*n)gq`Fj&nFLRtS#_gRlSBhSWL!7%X1Dx|Arr2(CZe?TSbGv4HPdhlj-_2Wc8 zq0?L)1v3R4K&k#rD-kQSZrM7falxZ9G@F}#&qsv}6X1`sez@+-P^8vZ#}br%@9kv9 zg?ws6&+5{FEhyp~D)E@aqY5{`@aJ}0&nbpJdWZnleo<{!$$SiQvyjvds*4eys9s~Q zPoiB1iRoi@T?!~#ed+8iu)}kd5~uePVG$bVqLf|YUuOr}!e@U@nr=)M5A|KCF%8lh zOg-E#UiuGTpL;tIVK9q?BsTM#<3tPMv*7HK#)ihTgr6QidJAw$vT{l}Mk< z%kXbVS)HqU!WzG0mTi8X&eJ*MNf2kW&`j(Y!%hs-70$#oyc}!X9srXqCUvc`JE%#4 z(ilp=+=h~p6so@Am>9#;d{!gA->?YHDwneIvMlvrhBG2Mg|(Y&(r>Z0w5Ik;FLB=Z zc+#(>V_QU31YH1d(1CwY=#2SUqwJ#(N4KXHk@x5U%S%E7ttzUQj=j}9dMjFq2G@`+ zZvoBc*-u9g2dfM4CyGmVyMe2I-S-z!;!f##e=5S97teRLyHaB|v|{AjP%k$P>ePjEGMFMDi%v$?o~ zsgLWIY$e3tW#{i=o$|`8qu*y1If?LLWrBQ+npJzG1&t<9(5%Nz!zkx@19TmcBfewV z5doD5!MDGYdtkLsvs4R$QSc3`UQGX7we3cY94mz7Ik78Rjs@pk$6`!A!|u0l`@DZM z{{iUfOW#>f=8t${EKwC%(=DhU^7I-Y^vdq|HRK`v4Q#Oe4@|FKAR)n8ApDS#0RN1< z|9R&BemX9`Ug3#oH=}6Ch87~NO}5rBXW#HR&~c1?t(6ikTobCSlbV#NvJUtSitCtROIu@7b`_(SiJo#mU z$+PM~MG48_;TAT%%yIY{BN?$F~ znQ(G1frM_J9g&zg#1TC*nmkrpq{C{_Ippjd*F6=p6ExZ}(`+lEdNKp1UGFcPYs&e+ ztNk|B3LA?_y^+p=-OS_Hk%nsgq8&62AWWledk2ZTP1&N{N~O!ufL1xO8b1Y8u$F)T z6ra|^jJ3E&H1(DAph-Ndxp&_o^D1fY8}+kt$b!F7DGq%SDT}}9%>IrG`oxPhubdu5 zIjQN=e%kaYE7D-tH~DmDNWZok$J8TOliu?l%3Q<qYaF0!#VEg>4qLGt zzCo{az0Rb5=X-wBjbmy|K;)EI8B29)dLDUTXDItVe7u<+ zaBQ`keGqCFo-|2m48_(Vr(!(24O&Ai4GaV+j4lx(;lpjhq=eu~oQ>&>LON<3ZRRZ- zGc%qpSHO+AxDBl$)iU3)drFa(&Nc^57pK&oee2?6Y>nP2p1n{Ff^Zz3|8CQe#bz=3 z9lD22yxkU#Lcf-VuMP2T8uAMNA3_=EJB|0qgwfYDT{AU>6!FvTyG%IAk03d=*=E}> z{~)qFhFIA=iT&vtxO1T$o9NEzK?|(!^K@GiJ5A;^I1^0U;#7Y;wHDY5zk@L9njzu1 zn&EP@jt6aqoc^z+ia@M>hN^B#Y01qAEVefB2D1ise+Dll;Anvv$?BJ`ll*%15f@mU zbk@|}sV+uhVv%i5`fS#djw@?e$6Dui0oI4GU-&hgN#8LS;bWO>VM`jt^l z2zJ9G8J!jVOo0&4R0z;4uZoJ;N7Ny$V}V`b4Mv72c&%Xp95ppY=-#_SgGY03v)ZVN z_NCv~twBMG(Qj-_(;BjcyZ6U?N@73MQIySnqx`=-9G$w`R34**rGIKUhh{IMlL=jJD(|RE4OELPF zii!#icqOGO#hLO?%$P`&n4%oGFiY^$Ic%_a7Tw8{{_mK~@`A}NmW9FD{{yT4FCtT7 zdW2&&I*w*stAG7U_hGgMKAqgD=Ok^R-M-cXi;_3tRP9YeJxNj#aX084`d^<4z0&K5 zgCqMm?=49zl>u)uz&tu4wunAi@il$j!K+4?9KXT}l3E+G{^DwWuf=$b7L3-XpRXH5 zQyv?WGM)0Odka^wg}Oqqqur442uWiXvf%9C#Dr)@r*ceE;jc_~$qNuHR%fiGlKdpC zK^pXg2$%GbZ64cTU)o3jk(Q$t#)Pz^B6%j4J@%)`^7}TExwpeXiGqVeChNe3|K$Z3 zOxt;D)?YWRx5gV@1Tc#MT@hnmy-BNQ4Eqhi?h1F&2K&T_+otyyK%B6F4-}AmcS-vO z*xzPYqI)$9Ti(#E@yuzQU$zs4tU}624_4HdqDAWk00^kuSxWCu_A8{Ex_3Og3<(nt zp|w^9fbN-a{(hk`CDVvH-7kMPAIYixeFhm}i>r&AY%lUVH#xPaxB4{E%qBhfU-ZPY z`yyox@3mmMQvL@V6-NI^_U~qGS;#S1Y9-wj-(M?yuFk_#qOezzZgwCDUs6R#Mx1TS zorhQdUK&|pD-x5R=ZOUzjn#Z?&2RU*|Kpn_*Va(67yT3F=KmaMvMNb@mZ1=b)WaJY z4%|416rnY8I(3?F-0wyg-Fx+u*9a(x$#1RS7X&y(Uk&=^^igR_ay#Iw^!12{2R4uw zZ_UWZoay`Da}V?M$mjl_OAZU42QudzR=!7<&#qq*Jkvcp++%1i=j`2HJDw*pIV@yo zu5U2{KG5Yj^Kr<2sz!3?e%j-+$9|ZrmfNNWTPL}5b|q!Lz8iS{Xi13fa*0Z9w{-K& zZ@ac4uvzxlFz#p2A#MC7@2mKFTZlM6CO;XRLfOha4MrEfx$#DF#IV(h*FAEN?q3;k zg{Z}*YP_x?Iiu(7d~y{UYs_rCLnMGQRg44#4Ne+Ga~S{&5>g71Vfr7ZDa1rDZ*MVD zE(XRAwBl`x5UeeBs)^4R0NH%hwJx>JSKgR-BSVkL6qM@t0QpLizf168#VY8zT<^M$ z6Y+{hVOqHg)ItNzxWO18?(Ny*71V)cX?XI@Mz!au{-K>e+|@`HTTwPHq7pV9OLq3a zjaR-$r2pCcO8wT?GIYuy`6(tSeEr2>oTP9g)8jL=;dZ+@S%dlcs(dYW?uK zp}b7z3E2$KNo;~(oTetoJGTNu>d%0@gIh!L=ZO{RjWK_}?@ifN#3c!TdVI3RuGR4z z$9A}cj8V>i3(D;4Xg&^n`YK}SrX)@Szey)&bf0#eu~T=-0q_wwx;!i^-X#vG?E)g^ zTbpZKMq3WwAwkS(HJQ<)yO!ir6k3quH<;irnQB*vj4<>u=)W|+SG{~EP14}sM#|se zIz~uhRZ)^nbNrycQx0JtQ2s>dq3s;M5?N^Apugk8rYl2)eBb@^M^-oc^T=C7DP#2? za16qV8hfc$7TzNV;cZgbK1QF-m1d$N%gCdimJPr zUpi$#`bzxZ!|H+h;;E=@k$9&um}3$~pfV4aATlD=mr&s%sL@cqe4t=2j%la97Fge=)lMpYfKG9Pa3;isDC1*Ky=^`|qTg2mL;7u|IFinlntM?Dk{v@vuu?HT^u2NU_jm^ z13Sp|Jwjpb&xRt=36O$}K+IAHq$=*kN?u3-00D==mT4$%%>zZD`(Kh;9} z2o7G!_d{6nv2lGQz8je? zMwvK#?B!;77*w6N4|}fSX~&(HhmY-Boi(cO+z(JN#su50bM|H^6Eh1CB^l$ zM5eVhLyGmBQ4H@nW2j(Ebe^8ba^R=$!6WyY(9S>Oe*5l^+D%NQU`e!=k;k#FsI`%j zy9OE-pUODc_HYNa1^t!o>#|l23JN(GK;5%&IY0;yESVKv`okA76t@jk08@46x)^m> z@>AfCri*k@AsxfhQ>{Xz6cfuauCKbUypJD)X$7*ugPJ-!1npaR<(*wwc}SoS?@fL{ zCJ!n*$a$B3jreoe=+=Hj%kbC?g*Ea;3crTsfC-jiTyE9vPI~krug#YU@UKp057dMK1ziGnf&DOIZ!;ZQMK0eL2}$Pwffs1p{~$@6i`W zLwIRREo43A`i!}vGG!$3XaQIoySTdTs}+mU)Z!SJ`AQ#VG>E_ixYNi5BCgPVC^0xs zL$1$O7uXn^{q-Z0&BkaWcsS{d40NNFlL;@*g;&=ZdTd6Z+!a5@nik6DQ6aORhN+N- zH&@kmyfFND4&A+qlq(}C5j!!6?l>~g{H?V7tg!gb>1M?b=bNmb-EL{RPfSJ#@!I*B z%oo)vbUMX5eYczndc}mnM@_VMZp1bz3*ygXKT{^Q;&|IGZ0W@ue=o=>&|{cwWU`!j zk~5g5DF*vE4333M`7IWn=|@)$4qX|+0kmxWB#wXX2%Q}r-LJwjL?PAkW|DjNZEINx z_eY_s(!4?3!#RYd#~AYT=xKF{6m%=NwP56;2lyfq>)Ki^Txvv6?_% z#A|H7sE^u}0_~g*J#-G;GQoXvM%mbX+7;&MJIvyn7T5+U5f2jXj&Rm`i@V?K@!5dA z@i2yPsKj{egfVEp&7r2H3fq2ux5m;31E}*Q8FEVbek!GGh56qL+O^#le5On=Cgp`o zH0p1w>?OASRsZK!Pm&qyoG)+M2l&Q>(N)gJ^p9g64etJQhwT{${0WR zQuBJ;1#A_phoh z3Qn*i|6`K=>ff3R*O&o9@{fA2S|~OC5mGW>I9f)0E+{u@;VC&5FT3#uEN zN-Q^<7|B_s#AkZY*Gmk~5&Io?Yfo#|x7rJgm+-d7rS&T-zALe2N2FitNR1vT@&X4q;;L!>j z%>GFKzOKu{!p2X_aa&3PK}SdEK?s!Ab-op+Gj3!E8;RZjfqC?8R-6EL66!nNgTXXn zQvG4KBs4sLr$fo4vh;&2HqH{i%!Payv_(#_Cou`q`k_EWPPpfCDi9t~3I3QXBhONZ z#9pS;r^j8PF6F+gjQLp7&EoRg*G>7Kkh7|0j~-fP?V6vs%a+MD8JENwbP_pD3kqNf z^#v4DSXLF^19`o)Ao=2wkGq%62#*vZ_I)$;&Du&>{J(9U0QP%So~ULj3^ z_Q)sf>cj~20p$&un4j=VwO-Yf5_317mnW>^W_^$*I+#wL|B7^dD*K7s0K8z?DaUH? zbdmks8OgVb^bh~KHY#-TTI^v1gDTm!9VK#Jw#_KSbyNUxRP6cTW83?1{I2N|W}US+ z-vUj9?i50a;Q}s+0-}oN{H$V@-FcWy*;0hkz{!8^K76NoQP7{Z7}^ zfu)v;?n70w@_jUcUPu1F=|(pFqwdJK46?~31}zbtHs2twA7tejGS)E0+oX*3oKQcY zLyOboIgc%@>2&Gjf12wfWE(z=YudV*n5EHbNWftN9;8NX8MNhQk0L?Frww}U)yvF$nk}BiYZba8F|(Z?Zhn}j6oPQ zCMxAzZr$uB%t7(Gw``=!L#fA-8fGgp@VRvIE_igqyMi`3Mo?F@Xp3J%c_}{{U_|AQ ziD^pd^9gz4!L{?UihC=8sd9I-L%;0YeA&t%_~yKWv3?_fQ4s$7aVnZnY<91U9@mai ztFW=q9a^e*#DS^2iclv`xIg5&BC*-^mQ~o-L=K#{&j%z?6wh+g=x{?1Y4@MvKV_dYiygjOV9pXsc{8Hiu1V9Ksyg~p^5=f^ z5V#J5L6H$L4!_mlpdvE5S^B!t;Et-yJyEA1>6>576pB@>>1XYiRqgg&Z`{$o#W4+I z)P~pJUYrHFnH^W^8L?q&RS@gamtxva+KSES;jJB9PMjKi)%pxsGy%8nW71Ubif)F) zNMq=$p(iqg!Ex|e(Bix3pi7Z2rcWu$XaItt(yUCZYexYIZ_C;nUn$`!GBFiZ-N&72 zew6tm7w1F*8v{j7L} z41TOA{!OG5PD{CM*T`ZTEYMQ1OsJP58C;;r{j4d;ObtvwdoMSQgY+32CXyY|rgbO% zgPtA-n6PcX0bSoz_kYvOP8dPttLD)aKks?T2}l0~$oU=uceB|vp;su%$g!pLC`op6 zAta|9CSpjS_*7g*S#F;66CzRHi#K~;Wxhu9z`X&GA{ItWxS^|s8_ps-(YzUQEK9{@ zfCLI%2HxXy0cSOPYY{)*1h|5#xQq6?aysVy>Mc|#Y*skBZNjnbx*}_nFfF!6;+O~e?12^%7`kP z4g4L3r=P2?-_@o2bmA;?YKvwrFB_PMR3-2)?#V}p1HQ3#T+Cm=U_~}q{nD|8HQ8jsf=T&dyj|xekC+u_@<%k@#m&?EyjjquC zOM#9{VvJWSvqQ{nQ>euwziIkgbAP(dnTq zBf+q3=i%oElGRUsUH_Fs%hQblx1-ue&wrqe|N?}X9Oyr z+sz_p5((6WT+hDfS+$$erzc1+FiIA3rT$#AwF-aR{TND$#;v#emM~eY5RVbPwsOnk-}w# zUKPhepp`CiP*$7rcmF9Jxn7#g4R_g{!f^`F*4E|#*?+kZjeIy~vt zHy#CGhDp9TxQQURgST)cGq(O>#rPhtacq4d$C=)zkw_jlm?j>-%WfNdsb1&U6B(Pv zS#G(vYO2;XS&P@R1SMiV57c%I#a(SZGeAyuG*%c$`t}3Y(gXI(^csmUAFweC`H*}( zit=m_2RQrf^oDPU0S|+=*Y3(>=Ntl4B5z0TU4+CWX-n6bprh>rjP=&!NHP_YOxWbq zwu_GDL`a$r^z!hH#;TXTcEcu{tT-Op@4k9X(SC>O^wA)lB6JL&^7#{YG}$k3M{njc zSCHj}vzIv#4)}GL+C^gH`qqtd z56vxJ+9pS|K-M8UJb@v2ijGjDfipKG#0!jvw!wC|kAGi4b)P21Ck9}YoAzB=(S)GS z{iG#!E};LCtUJ`4J94NsG|U@)YNZb}ZURo`ql=-p(oDob>0> zK%lOx6@J@ptN+H2{x=>J6RGt*5;N?L{wo)YW0kYRrgppV8tRAj@h+j?t%8VHnLmy( zv3T(WQtXlf9utu7y7R&BpdhwtIlCUMQerF8d$h?Y&twXS;G3*VJGY!wp^AoUG3P_4 z6~GHpmQF)SA%g+srP~#D7{~P_r9WsHa{;PjA8I;h!RQ_m%4I0b5&(aR2iN#Q;V&eekpNN<0VT+>mbSOtw0|VT2dhvtUlu#hB!%Ml zO&NuTcvj0@%|{7U!1a^5<1~EDNV?^Qh9?4#`#|@{U>13~fP32+T_dSMSPgia} zf?w$kZ^y+i9t-4?j_ivRwX<{^a~A&k3LX7THbhSy0PeB~-np9jv50T9nxDZYsNbz8cNhjg>lw8zm0r9yIZNPg(7CB-pD*k^9OOqa6Vt z#uZFx#AEwppN&erM<=x5=*Y^tz=_sfZ#@d4eKs8%67aG7HH@bzMipv*(v~XgCi zwR7zJR}KxIEo2;5YST6cM7}>_Z%mgJriNy`=BZ%`GWJR_I7uG(ZDa4kC^=QF55^p>wM|nh^{2)NARNp5%MT`uyubbEGIAk(q<<9sF3AZ{<~Q4Lcyj@ai=k zbv)vuz2@AuFLCOR6KpVYewz#Qrz|{8GNmaVk{<%nF?qDO^q|c~;ow(ljn^5eE=TkE zmdbU>*M-#(kLI{JRtH&waf$3BHD^akyQem)QC;2jvRqjY2U$kQ59N$}(|XO;Ir(o5 zYW1Z!x&`$HoMZ3uY7H*Izy31bK5$_bn#?oS>$W0c3J7$Gk(HFjINM~?LoPhC#sg$) zD#Xb9+Kb9YWmRrZnEYt~U*b{!@m)M?;F9#rl;|ZLLoSfSq>(pJW&XMYwn3B+%+|6n z8{3D2HM~y&dH7dobG0z5$B41{q@YP*41JUMzl1zb-&PIx9EPELS)stAAC4Jy3V%jN z_davRa$nFa(o4hm&3JPLy>JQUsO*C;RJr%|dkhY}1b z*H@xo8-DwemC6m6C}_6 zgFv^43XGDD_uP6f()7eNBD~P5NgtBaK7)%%63I3&E=yf=6fV2?Z*l|;UEo1bq>zC2 z3|=335qh|=f_mvM1Uxo_Y@r+!BGQ(k{Q;mj#oxmLddC<274C)^XBcyj+xUpCg1GoZ zTcwS^*dyjsH+-aU-DC*BX@7j|@3+?dTamNzcL%D6h)BUKv@Eb|z>17@jEGIgoluk- zC>dv(;O3{tFQH&(4X|{Ly*>id{f$(c)b`JuNBsBGc98f)q(5TF+3&Ix-kG72eS6{WpAgJQ)4_< zBFukNe$LT`8RE<_1ji|1rO=0NL(_Ra@wv2kPh*Z%l{FR50wt9Jk3X#Q;sYM3>bQ<+ zz|;{j#LUvJEx>l!fnP^jVoet6O>u_Hlv&*JP@WT|3mj#OBj{tXJ4KsB>Rs@Qkox_O zSOH4y=H3IIx+c}BJ8gI9c+^Tm7aGmb{1Ee5SMwQnrlObu^YjHPhou%^OV6Ta;br2d zo;|GCQYr6;vjM)L%8Q+mAmfKk0@(f2$Yo|fq6Bsb zb?PGoPSM~_V1`#zM2cm`<~K<7X7;F{)lwu{wMw!iorCXKupxxM7dK9uEjrl@(V#dz-$K6(pC1`WVbtcdVzpOv?##Li@=; z-OosU0aRXQr5ir4F(r&?eih5qtsul3d*(}~l^4I#rsI=EX5giP6okR6tIi?D&7(Gy zRJ_?CLrirN;I*Fo;WipVoT=lYm74PRu=-zYH!=Qm!`bRK893MU@Y$cwxWYE;gU3YM5L@~1${6e&_NSdX4l z%-59m>%SrT>{n3ibs->(&5Bp+AxPyi*}U^euFy}w=bU9vR7O1m}R9?@1=cCkiJHQ~h&Oh1rLi zQL;qP;=pV%NY8Wr*TCSG-^hy+G?`-FyE7T=79`rKyi+oL6AbQmC_~ag!e9;r8y@wm$w6?A zh%aCW&SAa7{l~X|_A*u8q=e%CZ+g0pBbE-k`xObP_NoJ9s@*e*;^FC^AvTCE1fE5l z-x6$ZgD14a$ZA7J)EvBi@>}CkUzULtcYT&kfSs=NfV2~IEq=>*~rEaxRqP*vJy~O;9D(4Lxdbg_i`5l_-BYF(aJ@TGBl7|2wLIUDA1bu2N z1_xfkUJ74kn~pQAOw_=V0YMq+$?VQkbW9l2;Ap6m8q5jN$cP|i+Zli?mwTFVqOrjF z%ueVd96Iz>ppN}*T`d6fFF|ZY+{G9CxPOWtAO7YjYl&8-|C~bY?NP0Be zN}g>_IC8dr$UytY-#@S2=MX)7k2-BS!k}Zs<=O1FY$edKPmjR~DQ2aQ^GUa^072I2 zu@|@By*`eB=e2KyPc0DpJVuA)fh$!X_n+9Dfn2m30Y zFJEajI1eLgqJh(Rm}c5iKev4YA0WcPTzj4g6t{!Y>*vF4Auy?^{9TP3DdI~{2mjHJ zsr4@e#J5)U%P#Vb$IS;OuzD2(#C(^g*F_mFVe;*l!%z1oCyZ+jurt&45{A`guQHtb ziK?7T)dx{t66o5xe8on?k*~VIk2=X_xNreCuzL)Zj|VyX`y2p5wQb#oIq%)k_Sd<& zZ}#v5(_cjRjfW1Zg2p1tUw;d43o)=%zGhJA6o0z=b(6xa2WtOgv^h9TiE4`X> zyL1EZpq5@3X#L=xG0~4h8$_?@u~nkT*_4#%O%bClBE)24RtkPvDD(tSuJpnDc=^_b zYHi0@TD9UgmhY!nN$d@J3?1JY7}B|0dOJ0&7|Gjk%~!y@sUMJ;LG#SkliW(A>LrU0 z-c3xXFx zRXTpYBcC%w+mhxb2&OjaARe3yMhQsKG)@)nI3)D^G}oimT5z5C04GGiY3hMirVJb= z`FDFFpATNG&)&;Y!ADmUKfz1(@R3~ckz4fPB-ZG&viYw(iZZl!Ba&z`BTjk*TZ#@Z$u!PE)?-cHWa0I=ppb7 z1GRK|!I1yM*jqN+k>6HQyn17V$C`nF-jcqu>3 zZp=;V@g%_H`LybJ`mFA2UXx63n#WOtoKkA`BJZmg`M$|e@QD}{ibQ&$&<$mg;Fi!| z?(IXc?gj1Nt#R$;8>h_J4FBWg{+|r{%MyvMGhea@xaK;ye7Zf3oS5YO(h=C|0uxxJ z`alH3q4F8oyU^4ePz03ikZb# z9gTA!IjBIhJIsunPz`avdRe5gtUb_%Kb!{0y%F{AnL@I5azI6Dv90;*K|I=> z0o`L^;CKdoy@X;7|Joh9mRFPU0kk*Ro0`(A`t*^tnw$8N7_jjLLZE&DeaT!Yf=cT=} zA3x6irH2a*-&8Uq%;ll(dey&Jp!EB*U`l8oUja}2`)@-p{5r)-gCV>LBYThEu8|{u zn#2bd3ZXF)3|)xnS7kCzYh~uRL3(y*(u(N5OOCW}8h?!Yrm2zUHeVtkNH>4ul|D)7 zAx{gt$|tu{&aL41#sOy_wY(nov|2@r6@FUpfyv-ku?{1o-EGOfyd`ejOx|Ojdcjk- zsqTmk#oc(Z>Gt36GekqSZ(GURxT_$X&-?^bA{44pRGFpK=Es?vGIcF#bT%v04TKj> zH|I}F}574UgGvumiW`;ZP823>savtmy5;( zP8uECB4${h8!i5=TH=2;5B%xEsE@~<+;97I&a1Lmrt`4#{f6DVa4^lMsq@>8L@YU7 z^ZLuO>GJRf84(`l*zRSZ^0xpwPnM}(KD2_-bEN+}TMb{dNz6d(N%$A5>Hj#*|M|2+ zfc)=lU0A4E#DmV(gTFXf9Jasn;`=qQi-Yr(91V8@!@393N|kRg3PaTS-IF5ASRJ7Q zRE%Vu;IjNBoZwaavjp7pJZhSB5lBcnF|ydWiL2>3YR&+0Qc>7X;@RL1c9N89Kq%bUWI%);L?_4a2zaW88*8f2I3e;2Q`D%ZGDv}K_JKzBO^3ggVS z$v)76+8PVrI~S+xJY=yg=DDCsFAxA9V^x8>yjsaNdky}s(i|1rM;pe2aO2SKbyfC< zbDs@wkA72NydIb&)~ClE-ARSHm(&xsy;)3 z-KpJd*;W2K%i1MG26HygUG*@CkHDx1d&mIvh zO-6Z@{D*9-2dC*T<3;GrbYdf!Ny%{j8Rs>OZ7kc(^^>$J;+Z5)Mi8=Pa6K^TQ?KqB z{TLab7ojX3lR{T*Qm`v2yH@r0wIId>A~N|s95(#7DRCq3!Bn6@hWd3TEGR-=_$e=# z^ax_fcC^%PyVR_8Cy)1Mk>xtyjSTeo^OW#WP4U%HW0=-WbNjIKV%-Jx^SHX30}aV9 z`qW3gQZ4?Tx&S2g!C#wKBFL24cVv5|%ShiuD$=1HUPRDv7lyr`geJZYhXd7VJ)f1v{~sO=6BI6Lr=gd$sib22I}O>dZWTU=KJk)$$10lFNHd z=o$1i0L8R`bNMaiQOafNJbGCP(=nVstj_02o=cjq9S&w+sKH?56gQ=#I_mCQHKgY7 z8}njc@HEy+!EGbdh)Q7$Zi1#07@=PYi3s+ZeXSn*#=pQJf>4UNI>>eWt)o4nLS)X|JVyVW4XAR*FVr+MA}j8gE2>Wh(Yfr9P#5YUHEz1ylD4&$tB&$i?I z<2(x^9)(0Y=1##Lh8$sznGD-^-MNhDJeA*{spry~7e17`TQ9TqcRj82CtP9zf@8Ou zNA4pqRx8-H2{JmUGI7qhJNfPPc$s!*Y3?^N-g3Eq$Xs>5LYENsMw7RkWxE!kkONQR z1OvDiVDPti*)YwjeY(O*DGIu`zjz}gh)6Ad(r;!>`uf0|X#`rS0~lBUekm)~1gtZ9 z&$Ap~w+Lp!wLQaTnWZZbpxrB0hKXaWpgkRjIHsq4s2}kw?5af-zrT2+51PfK?mDZf97)AO(?J+sl^{*=2r=iKs*_8U7~MekNYx8RY+lg^zAc z^EghVkAGP*V9BAxexBA1=;pjB$<*ifJz23G8#7>us9#6F#^D(ue{pZY3k1un^nW>B zXm38Bzm$T9s)jHFk5j~;@7A(py46DP3V+zkAudj>hk9=%21P2Dw~#RwUnm?HzYwUJ z-AW`&g#AAaxfi$nueTV`!*F+&3mk$ zh5LD`#>j~Elta!WN=6Nt;Jc5kwv2h^A51*UC3=A>L~NUH(HUn2L;I~=BgUb~7d`A% zfF4L`&9^UW(RU2(d4XB~Q9!G=sf-I=hr!~GS@C!|!Hs&wx4ejmN$MZjE~VnlUjX+v zgxsD0e&GasF67DuGBD#Z!Cy>+T-UtV-exz;PQtLj{M$ zx|nbOzo0?|gD$^;^_|^HmORW>zE8Q+^iMY6di&GBUk$qRPMsq%#(|QZAjp{P=f*sR z@3}o>i2_NC*yn^qG^-)XuT^14wQ+{2Xb2kS(|!AkW05`V1qI@R-!3Gno!WmJ3F^R`f3F7@Qs8FQkHr&PbxX2Dh>eq&yR5L9vShNk|`mUiw54xpPtK|4j_ zJSDSrl*#0iU-M%FAByM8o4bK$*0))Z$3xPD-J`q+e|6~QHUBny4)+oc<`bM4`HY^+ zqElJSK_7J{Y8RQH|6HQ3%OVcrual1kN@W!6bq|=p=j+yIG{L*k?<>$o7R~UFR!w3h zwgVfBAG}Y)nVc<8nDH6|ZUZFVyrAIxfo&msHJ;XI!~jd~fDqVWF9#E!?^;-aqyGFY zZddC^yvU;j$gIj$kVclsA=|}2buQ~H?GCWIi`eU)^l*pn$3x%i=_29kUN;~AX_s6& zo}-c?ua_OYkk7}z=NZXWV=*;6%ZAe9u)d@~Fa_2h2_vLT9H?N2TXqe%!;rFEUNQh` zNk5>zALs(+!p3{9X0UNQ)<)#2bRPq3al^~qrP?ZE^S{1PT*L_r4DL;F*2WSf-0PSZ z#N#2C&I=mwE^)YmIf+Z#Wz3bu-9o>q?BOVyfP7m-e@cqpLDi(!iGzc+UZ=HWdFMzE zs4#Bg;g%gM`Qs*J?y+j8s)p(k_u;;q(8oN+fLl`s5$p8Udp_Yb6--vz+i7aEbPN001mG%G zHsyFTzdWVpJQImL>d~!Ru;evtk_c5rqj9SWLY%BtVb35YCSC1z>Dvqa@kaEQK%dej za+Ai_#AL9)g`p5rNMnJ@`*k9{#E83_R{(R0rH^xhhYl1c3qBFxeps%|Ypnb;2lIrO z4-0}ZnH=Rp6aw-e!CbO^`+}%b*TZ~xh-zv+e~o!&whV~F=YSQc9E|%ob`?yCg!d3=)ADjM~cq)J9RDB&a0$5pw{vX5J6tKGBPLmm3&3!JaxehH(4WcB zF5PeV#OJruP~l36tM6r*l5i*-Y1H}NVByoZ33bA+*r3212e-!*8Uc5>3PixCMh{Y+ z`lj+m3=kt3h&YPFHSEz0+4evIY$(AT+$5P7w0Eb;(t-~6HW=2U`F@o%BfCQ|EoGeZ zMJpd&fetU@Fc}#gAMohjUQNW;=!BJ5%!#`0rHPybcl*P4k@JtAp62v3(P#b$wI0XzuKW`!+KM`q8j!18q zW+|rLXHFK6#GUgLCy6lr^&mrMfO<@{us^Yy%sSoDckbFnWjvAr3xR^Y=4fII@+ij*r?z)cVNTiufsCKltQt+u3=4KW#J1}ZoE$(5G!Hmx;s;9=vy?$8!0Tl?oUwo0OcatV&f$#pv8OLyDx) z%%<@q3cB1>sj0;8eqU>X$cz!W}$hjfffguuw%K* zkp9pw`0)B|qs_MY`DyIr!Kgf(;V8_l~Nq9UAw-N## zYWBdN(#oFol|1yun5YQOUZ&wE!Cziq`)o_l@2g^qcFihuA2(uTH>DB;%~JwD16*Z> z>6)P4^OI}M5<~57m}8wkR^oxIF*N#!yt+o3sV)J(O}>+g`94!-l>(yrzRUNa%Y5@V zRv%p34eSlwf<_S%^0OGXig2+Pn=QzTho!Q;P78jt@c=M=M$GS)yfIGQk^vja+}?Y= z5Z)iOy9hB)Yyqx6rgZlYHDc;w+(@u)rFP1Hz{X~0*v;ZnWB5KM<)S9hZk*k=B3vc< za4RJWeCdFrVil*W{mh37@Bp;sK-I+8O;!ZgZXVuim-q*K6>75F#RrN-bLY)9_w`UO zy{PVUJ1Z9413U0^;hSIJ$Z*#8%@{W}EZweS9(0LuOoRJ2kG7&P$g?X2m_EOO@ZG7m zK)&urSpn)Q&bgCMa67QR8tc~6HcZGObH;8^KQEB}?fv7$^L>0k!t+^JLZ^ej0pYH* z`?Lxc^xPd%K^Fh7L-s^p7`g&JTPTD_{sj)fbkNh61IIJF@_$p2Pkn$E0P^1;11w`0 zU~-BFQ+%4FBPEr8wb4>dSA}(~SHUW;A)v$GU#p;VRTu`e9s86{riBt-Ts+Vhl{@#j zPAFS#o)k5`sj*eS(iT{ag|(+Z%;&;-GLJImpe=1=yvIVyBCwCxF%+f$NrORknmRv| zwAB<3a^MCF02kn%DkQCVUs1XS zOEl3fLIcqF3J|yVCH<8DJ)kIi&MfjYl$xVZ#Y*ou6Hmob=bcGaEN{Loi^3y0=-^G; zxqljj6V^%WoIPviMya$Z{~l%eDAX3(j*yN-a0z8;-<*@s{z-)Q%qLU7=eX<4 zRVU}gFT_JRu+b5!*Y4YosKLllSRwh06AjUM41*>jxn584Llb{Ue=qQ;>KZa*(GQ*F zCu)TbygJeg0~}xTl4lt?n6FMtKK_(-zy&SNk7cMDA(%HA=`Rz89-{M$dpS)q!#OHL z({iFGzEq@J;n!8u$)9un!=T$)dPtHU;dQV)I?!gp4bGIFF1akg5!TOJmv^0h(3n={ zS~D>f{u=rh&+Mvv>)QQDNSt(Y|7zH1vbnD%f`NL&;#o&=kDw}4eF(ZtZm(Uu+e3l# zM+)&(dZ7Yg{9Hc3>=#Zuf794EsxmlgHvz1`3m>!G^pY&|?SBR|epGzLko&uytF}T5Vqg_wI=W+Zo{)U=|W6yo&{k6 z&-UhWBc^?6VtXFM_Uxm1VQO5I5v>fzc?otLK+h2mYFQDrt0k-3Zw55K;oCpp$UYk? z2}0eyD4^P?>c3FQa?C`2qUZO=heDGa{f{QsmMtctN~sL{ZGRE=lvbye)KO{#m^KU0 zA1zjQBf1p03TPtn-%yUUrW+C(M0`53$I68SFvhu5(z#e(kC&z|f%O>mJhJk~cwYtjVzL_U##`kxB_=0m2EA5^RARBa$sb5ez=sM+F8{_R35{D&5opyk)k>6`7OnU7g2(b`yM(DOD0=W5u3epq