From 621c3273c96bb4fa0b890df33c49d82de8cc7662 Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Thu, 10 Oct 2024 01:07:12 -0700 Subject: [PATCH] chore: get some more tests to work --- .github/workflows/build-project.yaml | 306 ++++++++++-------- .github/workflows/graalvm.yml | 2 +- .github/workflows/gradle.yml | 34 -- .java-version | 2 +- .jpb/jpb-settings.xml | 10 - .jpb/persistence-units.xml | 12 - .../downloads/akka/AkkaExtension.java | 34 +- .../downloads/akka/ProductionAkkaSystem.java | 7 +- akka/testkit/build.gradle.kts | 1 + .../test/akka/AkkaTestExtension.java | 19 +- .../artifact/api/mutation/Update.java | 75 +++++ .../artifact/api/query/ArtifactDetails.java | 57 +--- .../api/query/GetArtifactDetailsResponse.java | 45 +++ .../api/query/GetArtifactsResponse.java | 2 +- .../ArtifactRegistration.java | 40 +-- .../artifact/api/registration/Response.java | 50 +++ artifacts/events/build.gradle.kts | 4 - .../artifacts/events/ArtifactEvent.java | 10 +- .../artifacts/events/DetailsEvent.java | 3 +- artifacts/server/build.gradle.kts | 30 +- .../artifacts/server/Application.java | 3 +- .../details/ArtifactDetailsEntity.java | 80 +++-- .../{ => cmd}/details/DetailsCommand.java | 25 +- .../{ => cmd}/details/state/DetailsState.java | 6 +- .../{ => cmd}/details/state/EmptyState.java | 2 +- .../details/state/PopulatedState.java | 5 +- .../server/cmd/group/GroupCommand.java | 70 ++++ .../server/cmd/group/GroupEntity.java | 203 ++++++++++++ .../server/cmd/group/state/EmptyState.java | 54 ++++ .../server/cmd/group/state/GroupState.java | 40 +++ .../cmd/group/state/PopulatedState.java | 53 +++ .../transport/ArtifactCommandController.java | 168 ++++++++++ .../lib/git/GitRemoteValidationException.java | 9 + .../artifacts/server/lib/git/GitResolver.java | 66 ++++ .../server/query/group/domain/GroupOrg.java | 27 -- .../server/query/meta/ArtifactDto.java | 31 -- .../server/query/meta/ArtifactRepository.java | 7 +- .../server/query/meta/domain/Group.java | 12 +- .../server/query/meta/domain/JpaArtifact.java | 117 +++---- .../meta/domain/JpaArtifactTagValue.java | 12 +- .../ArtifactQueryController.java | 38 ++- .../src/main/resources/application.conf | 10 +- .../src/main/resources/application.yaml | 8 + .../main/resources/db/akka/akka_001_init.sql | 62 ++++ .../src/main/resources/db/akka/akka_2_8_2.xml | 14 + .../changelog/01-create-artifacts-schema.xml | 79 +++-- .../main/resources/db/liquibase-changelog.xml | 4 +- .../server/src/main/resources/logback.xml | 4 - .../artifacts/server/ApplicationTest.java | 29 ++ .../server/ArtifactRepositoryTest.java | 75 +---- .../test/artifacts/server/LiquibaseTest.java | 53 +++ .../cmd/details/ArtifactDetailsTest.java | 72 +++++ .../server/cmd/details/StateTest.java | 110 +++++++ .../cmd/transport/ArtifactControllerTest.java | 83 +++++ .../server/lib/git/GitResolverTest.java | 41 +++ .../src/test/resources/application-test.conf | 36 +++ .../src/test/resources/application-test.yaml | 4 +- .../changelog/1001-test-insert-artifacts.xml | 16 + .../src/test/resources/db/test-changelog.xml | 12 + .../worker/readside/ArtifactReadside.java | 17 +- gradle/libs.versions.toml | 17 +- settings.gradle.kts | 6 + 62 files changed, 1895 insertions(+), 628 deletions(-) delete mode 100644 .github/workflows/gradle.yml delete mode 100644 .jpb/jpb-settings.xml delete mode 100644 .jpb/persistence-units.xml create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/mutation/Update.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactDetailsResponse.java rename artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/{query => registration}/ArtifactRegistration.java (61%) create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/Response.java rename artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/{ => cmd}/details/ArtifactDetailsEntity.java (74%) rename artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/{ => cmd}/details/DetailsCommand.java (81%) rename artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/{ => cmd}/details/state/DetailsState.java (90%) rename artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/{ => cmd}/details/state/EmptyState.java (96%) rename artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/{ => cmd}/details/state/PopulatedState.java (95%) create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupCommand.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupEntity.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/EmptyState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/GroupState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/PopulatedState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/ArtifactCommandController.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitRemoteValidationException.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitResolver.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java rename artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/{meta => transport}/ArtifactQueryController.java (54%) create mode 100644 artifacts/server/src/main/resources/db/akka/akka_001_init.sql create mode 100644 artifacts/server/src/main/resources/db/akka/akka_2_8_2.xml create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ApplicationTest.java create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/LiquibaseTest.java create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/ArtifactDetailsTest.java create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/StateTest.java create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/transport/ArtifactControllerTest.java create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/lib/git/GitResolverTest.java create mode 100644 artifacts/server/src/test/resources/db/changelog/1001-test-insert-artifacts.xml create mode 100644 artifacts/server/src/test/resources/db/test-changelog.xml diff --git a/.github/workflows/build-project.yaml b/.github/workflows/build-project.yaml index 9a7caf33..692ef397 100644 --- a/.github/workflows/build-project.yaml +++ b/.github/workflows/build-project.yaml @@ -1,146 +1,174 @@ -name: Build and Verify +name: Build Images on: - push: - branches: [ master ] - pull_request: - branches: [ master ] + push: + branches: [ main, micronaut ] + pull_request: + branches: [ main, micronaut ] jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - project: [ 'artifact-impl', 'artifact-query-impl', 'auth-impl', 'version-synchronizer', 'versions-impl', 'versions-query-impl' ] + build-non-windows-image: + name: 'Build Non-Windows Image' + needs: [ build-jar-job ] + strategy: + matrix: + os: [ 'ubuntu-latest', 'macos-latest' ] + include: + - os: 'ubuntu-latest' + label: 'linux' + - os: 'macos-latest' + label: 'mac' + runs-on: ${{ matrix.os }} + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + - uses: graalvm/setup-graalvm@v1 + with: + java-version: '21' + distribution: 'graalvm' # See 'Options' for all available distributions + github-token: ${{ secrets.GITHUB_TOKEN }} + native-image-job-reports: 'true' + cache: 'gradle' + - name: Example step + run: | + echo "GRAALVM_HOME: $GRAALVM_HOME" + echo "JAVA_HOME: $JAVA_HOME" + java --version + native-image --version + - name: 'Publish Native Image' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: 'simple-socket-fn-logger-${{env.VERSION}}-${{matrix.label}}' + path: 'simple-socket-fn-logger-${{env.VERSION}}-all' + - name: 'Release Native Image Asset' + if: success() && contains(github.ref, 'v') + id: upload-release-asset + uses: actions/upload-release-asset@v1 env: - JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --add-opens=java.base/java.lang=ALL-UNNAMED - JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --add-opens=java.base/java.lang=ALL-UNNAMED - steps: - - uses: actions/checkout@v4 - - name: Set up SBT - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - name: Build with SBT - run: sbt -v ${{matrix.project}}/test - publish-maven: - needs: test - if: ${{ github.event_name == 'push' }} - runs-on: ubuntu-latest + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + upload_url: ${{env.UPLOAD_URL}} + asset_name: 'simple-socket-fn-logger-${{env.VERSION}}-${{matrix.label}}' + asset_path: 'simple-socket-fn-logger-${{env.VERSION}}-all' + asset_content_type: application/octet-stream + publish-image: + matrix: + + + needs: test + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up SBT + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Coursier cache + uses: coursier/cache-action@v6 + - name: Set up sponge repo credentials + run: | + mkdir ~/.sbt + echo "realm=Sonatype Nexus Repository Manager" >> ~/.sbt/sponge_creds + echo "host=repo-new.spongepowered.org" >> ~/.sbt/sponge_creds + echo "user=${{ secrets.SPONGE_MAVEN_REPO_USER }}" >> ~/.sbt/sponge_creds + echo "password=${{ secrets.SPONGE_MAVEN_REPO_PASSWORD }}" >> ~/.sbt/sponge_creds + cat ~/.sbt/sponge_creds + - name: Publish To Sponge Repos + run: | + sbt -v publish + rm -f ~/.sbt/sponge_creds + env: + REPO_NAME: "Sponge Repository" + REPO_CREDENTIAL_FILE: "sponge_creds" + CI_SYSTEM: Github Actions + GITHUB_USERNAME: "${{ github.actor }}" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + SONATYPE_SNAPSHOT_REPO: "${{ secrets.SPONGE_MAVEN_SNAPSHOT_REPO_URL }}" + SONATYPE_RELEASE_REPO: "${{ secrets.SPONGE_MAVEN_RELEASE_REPO_URL }}" + - name: Set up GitHub Packages credentials + run: | + mkdir -p ~/.sbt + rm -f ~/.sbt/.credentials + echo "realm=GitHub Packages" >> ~/.sbt/github_creds + echo "host=maven.pkg.github.com" >> ~/.sbt/github_creds + echo "user=${{ github.actor }}" >> ~/.sbt/github_creds + echo "password=${{ secrets.GITHUB_TOKEN }}" >> ~/.sbt/github_creds + - name: Publish to GitHub Packages + run: | + sbt -v publish + rm -f ~/.sbt/github_creds env: - JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --illegal-access=permit - JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --illegal-access=permit - steps: - - uses: actions/checkout@v4 - - name: Set up SBT - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - name: Coursier cache - uses: coursier/cache-action@v6 - - name: Set up sponge repo credentials - run: | - mkdir ~/.sbt - echo "realm=Sonatype Nexus Repository Manager" >> ~/.sbt/sponge_creds - echo "host=repo-new.spongepowered.org" >> ~/.sbt/sponge_creds - echo "user=${{ secrets.SPONGE_MAVEN_REPO_USER }}" >> ~/.sbt/sponge_creds - echo "password=${{ secrets.SPONGE_MAVEN_REPO_PASSWORD }}" >> ~/.sbt/sponge_creds - cat ~/.sbt/sponge_creds - - name: Publish To Sponge Repos - run: | - sbt -v publish - rm -f ~/.sbt/sponge_creds - env: - REPO_NAME: "Sponge Repository" - REPO_CREDENTIAL_FILE: "sponge_creds" - CI_SYSTEM: Github Actions - GITHUB_USERNAME: "${{ github.actor }}" - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - SONATYPE_SNAPSHOT_REPO: "${{ secrets.SPONGE_MAVEN_SNAPSHOT_REPO_URL }}" - SONATYPE_RELEASE_REPO: "${{ secrets.SPONGE_MAVEN_RELEASE_REPO_URL }}" - - name: Set up GitHub Packages credentials - run: | - mkdir -p ~/.sbt - rm -f ~/.sbt/.credentials - echo "realm=GitHub Packages" >> ~/.sbt/github_creds - echo "host=maven.pkg.github.com" >> ~/.sbt/github_creds - echo "user=${{ github.actor }}" >> ~/.sbt/github_creds - echo "password=${{ secrets.GITHUB_TOKEN }}" >> ~/.sbt/github_creds - - name: Publish to GitHub Packages - run: | - sbt -v publish - rm -f ~/.sbt/github_creds - env: - REPO_NAME: "GitHub Packages" - REPO_CREDENTIAL_FILE: "github_creds" - CI_SYSTEM: Github Actions - GITHUB_USERNAME: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONATYPE_SNAPSHOT_REPO: "https://maven.pkg.github.com/SpongePowered/SystemOfADownload" - SONATYPE_RELEASE_REPO: "https://maven.pkg.github.com/SpongePowered/SystemOfADownload" - - name: Set up Maven Central credentials - run: | - mkdir -p ~/.sbt - rm -f ~/.sbt/.credentials - echo "realm=Maven Central" >> ~/.sbt/sonatype - echo "host=oss.sonatype.org" >> ~/.sbt/sonatype - echo "user=${{ secrets.SPONGE_MAVEN_OSSRH_USER }}" >> ~/.sbt/sonatype - echo "password=${{ secrets.SPONGE_MAVEN_OSSRH_PASSWORD }}" >> ~/.sbt/sonatype - - name: Publish to Maven Central - run: | - sbt -v publish - rm -f ~/.sbt/sonatype - env: - REPO_NAME: "Maven Central" - REPO_CREDENTIAL_FILE: "sonatype" - CI_SYSTEM: Github Actions - GITHUB_USERNAME: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONATYPE_SNAPSHOT_REPO: "https://oss.sonatype.org/content/repositories/snapshots" - SONATYPE_RELEASE_REPO: "https://oss.sonatype.org/service/local/staging/deploy/maven2" - - name: Cleanup before cache - shell: bash - run: | - rm -rf "$HOME/.ivy2/local" || true - find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true - find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true - find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true - find $HOME/.sbt -name "*.lock" -delete || true - publish-docker-image: - needs: test - if: ${{ github.event_name == 'push' }} - runs-on: ubuntu-latest - strategy: - matrix: - project: [ 'artifact-impl', 'artifact-query-impl', 'auth-impl', 'version-synchronizer', 'versions-impl', 'versions-query-impl' ] + REPO_NAME: "GitHub Packages" + REPO_CREDENTIAL_FILE: "github_creds" + CI_SYSTEM: Github Actions + GITHUB_USERNAME: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONATYPE_SNAPSHOT_REPO: "https://maven.pkg.github.com/SpongePowered/SystemOfADownload" + SONATYPE_RELEASE_REPO: "https://maven.pkg.github.com/SpongePowered/SystemOfADownload" + - name: Set up Maven Central credentials + run: | + mkdir -p ~/.sbt + rm -f ~/.sbt/.credentials + echo "realm=Maven Central" >> ~/.sbt/sonatype + echo "host=oss.sonatype.org" >> ~/.sbt/sonatype + echo "user=${{ secrets.SPONGE_MAVEN_OSSRH_USER }}" >> ~/.sbt/sonatype + echo "password=${{ secrets.SPONGE_MAVEN_OSSRH_PASSWORD }}" >> ~/.sbt/sonatype + - name: Publish to Maven Central + run: | + sbt -v publish + rm -f ~/.sbt/sonatype env: - JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --illegal-access=permit - JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --illegal-access=permit - steps: - - uses: actions/checkout@v4 - - name: Set up SBT - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - name: Build with SBT - run: sbt -v ${{matrix.project}}/Docker/stage - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.3.0 - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v3 - with: - context: ./${{ matrix.project }}/target/docker/stage/ - platforms: linux/amd64,linux/arm64 - push: true - tags: ghcr.io/spongepowered/systemofadownload-${{matrix.project}}:latest,ghcr.io/spongepowered/systemofadownload-${{matrix.project}}:${{ github.sha }},ghcr.io/spongepowered/systemofadownload-${{matrix.project}}:${{ github.ref_name }} + REPO_NAME: "Maven Central" + REPO_CREDENTIAL_FILE: "sonatype" + CI_SYSTEM: Github Actions + GITHUB_USERNAME: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONATYPE_SNAPSHOT_REPO: "https://oss.sonatype.org/content/repositories/snapshots" + SONATYPE_RELEASE_REPO: "https://oss.sonatype.org/service/local/staging/deploy/maven2" + - name: Cleanup before cache + shell: bash + run: | + rm -rf "$HOME/.ivy2/local" || true + find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true + find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.sbt -name "*.lock" -delete || true + publish-docker-image: + needs: test + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + strategy: + matrix: + project: [ 'artifact-impl', 'artifact-query-impl', 'auth-impl', 'version-synchronizer', 'versions-impl', 'versions-query-impl' ] + env: + JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --illegal-access=permit + JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 --illegal-access=permit + steps: + - uses: actions/checkout@v3 + - name: Set up SBT + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Build with SBT + run: sbt -v ${{matrix.project}}/Docker/stage + - name: Set up QEMU + uses: docker/setup-qemu-action@v2.1.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.1.0 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: ./${{ matrix.project }}/target/docker/stage/ + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/spongepowered/systemofadownload-${{matrix.project}}:latest,ghcr.io/spongepowered/systemofadownload-${{matrix.project}}:${{ github.sha }},ghcr.io/spongepowered/systemofadownload-${{matrix.project}}:${{ github.ref_name }} diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 8bd322ca..ddab0cf0 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -31,5 +31,5 @@ jobs: DOCKER_REGISTRY_URL: ${{ secrets.DOCKER_REGISTRY_URL }} TESTCONTAINERS_RYUK_DISABLED: true run: | - export DOCKER_IMAGE=`echo "${DOCKER_REGISTRY_URL}/${DOCKER_REPOSITORY_PATH}/systemofadownload" | sed -e 's#//#/#' -e 's#^/##'` + export DOCKER_IMAGE=`echo "${DOCKER_REGISTRY_URL}/${DOCKER_REPOSITORY_PATH}/demo" | sed -e 's#//#/#' -e 's#^/##'` ./gradlew check dockerPushNative --no-daemon diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index 727100df..00000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Java CI -on: [push, pull_request] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - ~/.m2/repository - key: ${{ runner.os }}-gradle-test-${{ hashFiles('**/*.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle-test- - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - - name: Docker login - uses: docker/login-action@v3 - with: - registry: ${{ secrets.DOCKER_REGISTRY_URL }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build And Push Docker Image - env: - DOCKER_REPOSITORY_PATH: ${{ secrets.DOCKER_REPOSITORY_PATH }} - DOCKER_REGISTRY_URL: ${{ secrets.DOCKER_REGISTRY_URL }} - TESTCONTAINERS_RYUK_DISABLED: true - run: | - export DOCKER_IMAGE=`echo "${DOCKER_REGISTRY_URL}/${DOCKER_REPOSITORY_PATH}/systemofadownload" | sed -e 's#//#/#' -e 's#^/##'` - ./gradlew check dockerPush --no-daemon diff --git a/.java-version b/.java-version index fcc01369..aabe6ec3 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -20.0.1 +21 diff --git a/.jpb/jpb-settings.xml b/.jpb/jpb-settings.xml deleted file mode 100644 index 57462660..00000000 --- a/.jpb/jpb-settings.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.jpb/persistence-units.xml b/.jpb/persistence-units.xml deleted file mode 100644 index a462c23d..00000000 --- a/.jpb/persistence-units.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java index 5f24ff44..d871941b 100644 --- a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java +++ b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java @@ -4,11 +4,7 @@ import akka.actor.typed.Behavior; import akka.actor.typed.Scheduler; import akka.actor.typed.SpawnProtocol; -import akka.actor.typed.javadsl.Adapter; -import akka.actor.typed.javadsl.Behaviors; import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.management.cluster.bootstrap.ClusterBootstrap; -import akka.management.javadsl.AkkaManagement; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import io.micronaut.context.annotation.Bean; @@ -16,25 +12,53 @@ import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; +/** + * The Akka extension for an application. This provides the {@link ActorSystem} and {@link ClusterSharding} for the + * application. + */ @Factory public class AkkaExtension { + /** + * The {@link Scheduler} for the system. + * + * @param system The system to get the scheduler from + * @return The scheduler + */ @Bean public Scheduler systemScheduler(@NonNull ActorSystem system) { return system.scheduler(); } + /** + * The {@link Config} for the system. + * + * @return The config + */ @Bean public Config akkaConfig() { return ConfigFactory.defaultApplication(); } + /** + * The {@link ActorSystem} for the application. + * + * @param behavior The behavior to use for the system + * @param config The config to use for the system + * @return The actor system + */ @Singleton @Bean(preDestroy = "terminate") public ActorSystem system(@NonNull Behavior behavior, @NonNull Config config) { - return ActorSystem.create(behavior, "soad-master"); + return ActorSystem.create(behavior, "soad-master", config); } + /** + * The {@link ClusterSharding} for the application. + * + * @param system The system to get the sharding from + * @return The cluster sharding + */ @Bean @Singleton public ClusterSharding clusterSharding(@NonNull ActorSystem system) { diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java b/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java index 62c6baa1..cd971e55 100644 --- a/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java +++ b/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java @@ -1,6 +1,5 @@ package org.spongepowered.downloads.akka; -import akka.actor.typed.ActorSystem; import akka.actor.typed.Behavior; import akka.actor.typed.SpawnProtocol; import akka.actor.typed.javadsl.Behaviors; @@ -8,11 +7,15 @@ import akka.management.javadsl.AkkaManagement; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; -import io.micronaut.context.annotation.Requires; @Factory public class ProductionAkkaSystem { + /** + * The {@link Behavior} for the production guardian. + * + * @return The behavior + */ @Bean public Behavior productionGuardian() { return Behaviors.setup(ctx -> { diff --git a/akka/testkit/build.gradle.kts b/akka/testkit/build.gradle.kts index ff4b171c..b5d8759b 100644 --- a/akka/testkit/build.gradle.kts +++ b/akka/testkit/build.gradle.kts @@ -11,4 +11,5 @@ dependencies { api("io.micronaut:micronaut-inject") api(project(":akka")) api(libs.akka.testkit) + api(libs.akka.persistence.testkit) } diff --git a/akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java b/akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java index 69e37478..643f7002 100644 --- a/akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java +++ b/akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java @@ -5,17 +5,22 @@ import akka.actor.typed.ActorSystem; import akka.actor.typed.Behavior; import akka.actor.typed.SpawnProtocol; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.persistence.testkit.PersistenceTestKitPlugin; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; +import jakarta.inject.Inject; @Factory public class AkkaTestExtension { + @Replaces @Bean public Behavior testBehavior() { @@ -25,14 +30,22 @@ public Behavior testBehavior() { @Replaces @Bean public Config testConfig() { - return ConfigFactory.defaultApplication() + return PersistenceTestKitPlugin.getInstance().config() .withFallback(BehaviorTestKit.applicationTestConfig()) + .withFallback(ConfigFactory.defaultApplication()) .resolve(); } + @Singleton @Bean(preDestroy = "shutdownTestKit") - public ActorTestKit testKit() { - return ActorTestKit.create(); + public ActorTestKit testKit(final @NonNull Config config) { + return ActorTestKit.create(config); + } + + @Bean + @Replaces(bean = ClusterSharding.class) + public ClusterSharding cluster(final ActorSystem system) { + return ClusterSharding.get(system); } @Replaces(bean = ActorSystem.class) diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/mutation/Update.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/mutation/Update.java new file mode 100644 index 00000000..69bb381c --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/mutation/Update.java @@ -0,0 +1,75 @@ +package org.spongepowered.downloads.artifact.api.mutation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.vavr.control.Either; +import io.vavr.control.Try; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; + +import java.net.URI; +import java.net.URL; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") +@JsonDeserialize +public sealed interface Update { + + Either validate(); + + @JsonTypeName("website") + record Website( + @JsonProperty(required = true) String website + ) implements Update { + + @Override + public Either validate() { + return Try.of(() -> URI.create(this.website)) + .mapTry(URI::toURL) + .toEither() + .mapLeft(_ -> new ArtifactDetails.Response.Error(String.format("Invalid URL: %s", this.website))); + } + } + + @JsonTypeName("displayName") + record DisplayName( + @JsonProperty(required = true) String display + ) implements Update { + + @Override + public Either validate() { + return Either.right(this.display); + } + + } + + @JsonTypeName("issues") + record Issues( + @JsonProperty(required = true) String issues + ) implements Update { + + @Override + public Either validate() { + return Try.of(() -> URI.create(this.issues)) + .mapTry(URI::toURL) + .toEither() + .mapLeft(_ -> new ArtifactDetails.Response.Error(String.format("Invalid URL: %s", this.issues))); + } + } + + @JsonTypeName("git-repo") + record GitRepository( + @JsonProperty(required = true) String gitRepo + ) implements Update { + + @Override + public Either validate() { + return Try.of(() -> URI.create(this.gitRepo)) + .mapTry(URI::toURL) + .toEither() + .mapLeft(_ -> new ArtifactDetails.Response.Error(String.format("Invalid URL: %s", this.gitRepo))); + } + + } +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java index 56e6a717..e90806e7 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java @@ -24,66 +24,11 @@ */ package org.spongepowered.downloads.artifact.api.query; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.net.URL; - public final class ArtifactDetails { - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonDeserialize - public sealed interface Update { - - @JsonTypeName("website") - record Website( - @JsonProperty(required = true) String website - ) implements Update { - - @JsonCreator - public Website { - } - - } - - @JsonTypeName("displayName") - record DisplayName( - @JsonProperty(required = true) String display - ) implements Update { - - @JsonCreator - public DisplayName { - } - - } - - @JsonTypeName("issues") - record Issues( - @JsonProperty(required = true) String issues - ) implements Update { - @JsonCreator - public Issues { - } - - } - - @JsonTypeName("git-repo") - record GitRepository( - @JsonProperty(required = true) String gitRepo - ) implements Update { - - @JsonCreator - public GitRepository { - } - - } - } - @JsonSerialize @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) public sealed interface Response { @@ -99,6 +44,8 @@ record Ok( } record NotFound(String message) implements Response {} + + record Error(String message) implements Response {} } diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactDetailsResponse.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactDetailsResponse.java new file mode 100644 index 00000000..654feb60 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactDetailsResponse.java @@ -0,0 +1,45 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +import java.util.Map; +import java.util.SortedSet; + +@JsonSerialize +public sealed interface GetArtifactDetailsResponse { + + record RetrievedArtifact(ArtifactCoordinates coordinates, + String displayName, String website, String gitRepository, + String issues, + Map> tags) implements GetArtifactDetailsResponse { + } + + record MissingArtifact(ArtifactCoordinates coordinates) implements GetArtifactDetailsResponse { + } + +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java index 4c3de2a0..4d86d53a 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java @@ -40,7 +40,7 @@ public sealed interface GetArtifactsResponse { @JsonSerialize - record GroupMissing(@JsonProperty String groupRequested) implements GetArtifactsResponse { + record GroupMissing(@JsonProperty String groupId) implements GetArtifactsResponse { @JsonCreator public GroupMissing { diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java similarity index 61% rename from artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java rename to artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java index d7f0cc82..13d49b85 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java @@ -22,9 +22,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifact.api.query; +package org.spongepowered.downloads.artifact.api.registration; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -40,42 +41,7 @@ public record RegisterArtifact( ) { @JsonCreator - public RegisterArtifact(final String artifactId, final String displayName) { - this.artifactId = artifactId; - this.displayName = displayName; - } - + public RegisterArtifact {} } - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.GroupMissing.class, - name = "UnknownGroup"), - @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, - name = "RegisteredArtifact"), - @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, - name = "AlreadyRegistered"), - }) - public sealed interface Response { - - @JsonSerialize - record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { - - } - - @JsonSerialize - record ArtifactAlreadyRegistered( - @JsonProperty String artifactName, - @JsonProperty String groupId - ) implements Response { - - } - - @JsonSerialize - record GroupMissing(@JsonProperty("groupId") String s) implements Response { - - } - - } } diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/Response.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/Response.java new file mode 100644 index 00000000..bfb9d396 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/Response.java @@ -0,0 +1,50 @@ +package org.spongepowered.downloads.artifact.api.registration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = Response.GroupMissing.class, + name = "UnknownGroup"), + @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, + name = "RegisteredArtifact"), + @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, + name = "AlreadyRegistered"), +}) +public sealed interface Response { + + @JsonSerialize + record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { + + @JsonIgnore + public String groupId() { + return this.coordinates.groupId(); + } + + @JsonIgnore + public String artifactId() { + return this.coordinates.artifactId(); + } + + } + + @JsonSerialize + record ArtifactAlreadyRegistered( + @JsonProperty String artifactName, + @JsonProperty String groupId + ) implements Response { + + } + + @JsonSerialize + record GroupMissing(@JsonProperty("groupId") String s) implements Response { + + } + +} diff --git a/artifacts/events/build.gradle.kts b/artifacts/events/build.gradle.kts index d2abb374..56e6180c 100644 --- a/artifacts/events/build.gradle.kts +++ b/artifacts/events/build.gradle.kts @@ -4,10 +4,6 @@ plugins { `java-library` } -java { - sourceCompatibility = JavaVersion.toVersion("20") - targetCompatibility = JavaVersion.toVersion("20") -} dependencies { api(project(":artifacts:api")) diff --git a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java index 7afa5009..cd292218 100644 --- a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java +++ b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java @@ -18,7 +18,7 @@ default String partitionKey() { @JsonTypeName("registered") @JsonDeserialize - final record ArtifactRegistered( + record ArtifactRegistered( ArtifactCoordinates coordinates ) implements ArtifactEvent { @@ -29,7 +29,7 @@ final record ArtifactRegistered( @JsonTypeName("git-repository") @JsonDeserialize - final record GitRepositoryAssociated( + record GitRepositoryAssociated( ArtifactCoordinates coordinates, String repository ) implements ArtifactEvent { @@ -41,7 +41,7 @@ final record GitRepositoryAssociated( @JsonTypeName("website") @JsonDeserialize - final record WebsiteUpdated( + record WebsiteUpdated( ArtifactCoordinates coordinates, String url ) implements ArtifactEvent { @@ -53,7 +53,7 @@ final record WebsiteUpdated( @JsonTypeName("issues") @JsonDeserialize - final record IssuesUpdated( + record IssuesUpdated( ArtifactCoordinates coordinates, String url ) implements ArtifactEvent { @@ -65,7 +65,7 @@ final record IssuesUpdated( @JsonTypeName("displayName") @JsonDeserialize - final record DisplayNameUpdated( + record DisplayNameUpdated( ArtifactCoordinates coordinates, String displayName ) implements ArtifactEvent { diff --git a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java index eadddd99..e84d556d 100644 --- a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java +++ b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java @@ -25,7 +25,6 @@ package org.spongepowered.downloads.artifacts.events; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -35,6 +34,8 @@ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface DetailsEvent extends AkkaSerializable { + ArtifactCoordinates coordinates(); + @JsonDeserialize @JsonTypeName("registered") record ArtifactRegistered( diff --git a/artifacts/server/build.gradle.kts b/artifacts/server/build.gradle.kts index 48d5dc38..d62a5fba 100644 --- a/artifacts/server/build.gradle.kts +++ b/artifacts/server/build.gradle.kts @@ -4,6 +4,7 @@ import io.micronaut.testresources.buildtools.KnownModules plugins { id("io.micronaut.application") id("io.micronaut.test-resources") + id("io.micronaut.aot") id("com.github.johnrengelman.shadow") } @@ -23,6 +24,11 @@ micronaut { } } + +application { + mainClass.set("org.spongepowered.downloads.artifacts.server.Application") +} + tasks { test { useJUnitPlatform() @@ -42,9 +48,11 @@ dependencies { annotationProcessor("io.micronaut.data:micronaut-data-processor") annotationProcessor("io.micronaut.validation:micronaut-validation-processor") annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + annotationProcessor("io.micronaut:micronaut-http-validation") implementation("io.micronaut:micronaut-jackson-databind") implementation("io.micronaut.serde:micronaut-serde-jackson") implementation("io.micronaut:micronaut-http-server-netty") + implementation(libs.bundles.git) runtimeOnly("org.yaml:snakeyaml") @@ -54,19 +62,33 @@ dependencies { // databases implementation("io.micronaut.data:micronaut-data-r2dbc") -// implementation("io.micronaut.sql:micronaut-vertx-pg-client") -// implementation("io.micronaut.sql:micronaut-hibernate-reactive") implementation("io.vertx:vertx-pg-client") runtimeOnly(libs.postgres.r2dbc) + implementation("jakarta.persistence:jakarta.persistence-api:2.2.3") + // Liquibase required jdbc driver + implementation("io.micronaut.sql:micronaut-jdbc-hikari") runtimeOnly("org.postgresql:postgresql") - - + // Liquibase migrations implementation("io.micronaut.liquibase:micronaut-liquibase") + + // Micronaut implementation("io.micronaut:micronaut-http-client-jdk") + implementation("io.micronaut:micronaut-management") + +// implementation(libs.lightbend.management) +// implementation(libs.lightbend.bootstrap) +// implementation(libs.akka.discovery) + + implementation(libs.akka.diagnostics) + testImplementation("io.micronaut.testresources:micronaut-test-resources-extensions-junit-platform") testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("io.micronaut.test:micronaut-test-junit5") testImplementation(project(":akka:testkit")) + testImplementation(libs.akka.persistence.testkit) + testImplementation("org.testcontainers:r2dbc") + testImplementation("org.testcontainers:postgresql") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testResourcesService("org.postgresql:postgresql") compileOnly("org.graalvm.nativeimage:svm") diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java index bd12b8ba..5ef6c4e8 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java @@ -1,7 +1,6 @@ package org.spongepowered.downloads.artifacts.server; import akka.actor.typed.ActorSystem; -import akka.actor.typed.SpawnProtocol; import akka.cluster.sharding.typed.javadsl.ClusterSharding; import io.micronaut.context.annotation.Factory; import io.micronaut.runtime.Micronaut; @@ -21,7 +20,7 @@ public class Application { public Application( final ActorSystem system, final ClusterSharding sharding - ) { + ) { this.system = system; this.sharding = sharding; } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java similarity index 74% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java index f6462d9a..6e319c87 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.server.details; +package org.spongepowered.downloads.artifacts.server.cmd.details; import akka.NotUsed; import akka.actor.typed.Behavior; @@ -34,19 +34,21 @@ import akka.persistence.typed.javadsl.CommandHandlerWithReply; import akka.persistence.typed.javadsl.EventHandler; import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; -import io.micronaut.http.HttpResponse; +import io.vavr.control.Either; import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; import org.spongepowered.downloads.artifacts.events.DetailsEvent; -import org.spongepowered.downloads.artifacts.server.details.state.DetailsState; -import org.spongepowered.downloads.artifacts.server.details.state.EmptyState; -import org.spongepowered.downloads.artifacts.server.details.state.PopulatedState; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.DetailsState; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.EmptyState; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.PopulatedState; import java.util.List; +import java.util.Set; +import java.util.function.Function; public class ArtifactDetailsEntity extends EventSourcedBehaviorWithEnforcedReplies { - - private static final HttpResponse NOT_FOUND = HttpResponse.notFound(new ArtifactDetails.Response.NotFound("group or artifact not found")); + private static final Either NOT_FOUND = Either.left( + new ArtifactDetails.Response.NotFound("group or artifact not found")); public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create( DetailsCommand.class, "DetailsEntity"); private final String artifactId; @@ -135,13 +137,15 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.validUrl().toString())) .thenReply( cmd.replyTo(), - us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - )) + us -> Either.right( + new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + ) + ) ) ) .onCommand( @@ -150,13 +154,15 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.website().toString())) .thenReply( cmd.replyTo(), - us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - )) + us -> Either.right( + new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + ) + ) ) ) .onCommand( @@ -165,13 +171,15 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.displayName())) .thenReply( cmd.replyTo(), - us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - )) + us -> Either.right( + new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + ) + ) ) ) .onCommand( @@ -180,13 +188,15 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactGitRepositoryUpdated(s.coordinates(), cmd.gitRemote())) .thenReply( cmd.replyTo(), - us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - )) + us -> Either.right( + new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + ) + ) ) ); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java similarity index 81% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java index 04027110..92916d2c 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.server.details; +package org.spongepowered.downloads.artifacts.server.cmd.details; import akka.NotUsed; import akka.actor.typed.ActorRef; @@ -30,7 +30,6 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.micronaut.http.HttpResponse; import io.vavr.control.Either; import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; @@ -53,11 +52,11 @@ @JsonSubTypes.Type(value = DetailsCommand.UpdateDisplayName.class, name = "display-name") }) -public interface DetailsCommand extends AkkaSerializable { +public sealed interface DetailsCommand extends AkkaSerializable { @JsonDeserialize - final record RegisterArtifact(ArtifactCoordinates coordinates, - String displayName, ActorRef replyTo) + record RegisterArtifact(ArtifactCoordinates coordinates, + String displayName, ActorRef replyTo) implements DetailsCommand { @JsonCreator @@ -66,10 +65,10 @@ final record RegisterArtifact(ArtifactCoordinates coordinates, } @JsonDeserialize - final record UpdateWebsite( + record UpdateWebsite( ArtifactCoordinates coordinates, URL website, - ActorRef> replyTo + ActorRef> replyTo ) implements DetailsCommand { @JsonCreator @@ -78,10 +77,10 @@ final record UpdateWebsite( } @JsonDeserialize - final record UpdateDisplayName( + record UpdateDisplayName( ArtifactCoordinates coordinates, String displayName, - ActorRef> replyTo + ActorRef> replyTo ) implements DetailsCommand { @JsonCreator @@ -90,10 +89,10 @@ final record UpdateDisplayName( } @JsonDeserialize - final record UpdateGitRepository( + record UpdateGitRepository( ArtifactCoordinates coordinates, String gitRemote, - ActorRef> replyTo + ActorRef> replyTo ) implements DetailsCommand { @JsonCreator @@ -102,10 +101,10 @@ final record UpdateGitRepository( } @JsonDeserialize - final record UpdateIssues( + record UpdateIssues( ArtifactCoordinates coords, URL validUrl, - ActorRef> replyTo + ActorRef> replyTo ) implements DetailsCommand { @JsonCreator diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java similarity index 90% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java index 15d01fdd..05a47a94 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java @@ -22,14 +22,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.server.details.state; +package org.spongepowered.downloads.artifacts.server.cmd.details.state; import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -public sealed interface DetailsState extends AkkaSerializable - permits EmptyState, PopulatedState { - +public interface DetailsState extends AkkaSerializable { ArtifactCoordinates coordinates(); String displayName(); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/EmptyState.java similarity index 96% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/EmptyState.java index 2302278a..5a6a30a6 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/EmptyState.java @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.server.details.state; +package org.spongepowered.downloads.artifacts.server.cmd.details.state; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java similarity index 95% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java index 10cf2b76..9847f3a9 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java @@ -22,17 +22,18 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.server.details.state; +package org.spongepowered.downloads.artifacts.server.cmd.details.state; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; import org.spongepowered.downloads.artifacts.events.DetailsEvent; @JsonDeserialize public record PopulatedState(ArtifactCoordinates coordinates, String displayName, String website, String gitRepository, - String issues) implements DetailsState { + String issues) implements DetailsState, AkkaSerializable { @JsonCreator public PopulatedState { diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupCommand.java new file mode 100644 index 00000000..49963965 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupCommand.java @@ -0,0 +1,70 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.server.cmd.group; + +import akka.actor.typed.ActorRef; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.spongepowered.downloads.akka.AkkaSerializable; +import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; +import org.spongepowered.downloads.artifact.api.registration.Response; + + +@JsonSerialize +public sealed interface GroupCommand extends AkkaSerializable { + + record GetGroup( + String groupId, + ActorRef replyTo + ) implements GroupCommand { + + } + + @JsonSerialize + record GetArtifacts( + String groupId, + ActorRef replyTo + ) implements GroupCommand { + } + + @JsonSerialize + record RegisterArtifact( + String artifact, + ActorRef replyTo + ) implements GroupCommand { + + } + + record RegisterGroup( + String mavenCoordinates, + String name, + String website, + ActorRef replyTo + ) implements GroupCommand { + + } + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupEntity.java new file mode 100644 index 00000000..8bfdb6c6 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupEntity.java @@ -0,0 +1,203 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.server.cmd.group; + +import akka.cluster.sharding.typed.javadsl.EntityContext; +import akka.cluster.sharding.typed.javadsl.EntityTypeKey; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.CommandHandlerWithReply; +import akka.persistence.typed.javadsl.CommandHandlerWithReplyBuilder; +import akka.persistence.typed.javadsl.EffectFactories; +import akka.persistence.typed.javadsl.EventHandler; +import akka.persistence.typed.javadsl.EventHandlerBuilder; +import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; +import akka.persistence.typed.javadsl.ReplyEffect; +import akka.persistence.typed.javadsl.RetentionCriteria; +import io.vavr.collection.HashSet; +import io.vavr.control.Try; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; +import org.spongepowered.downloads.artifact.api.registration.Response; +import org.spongepowered.downloads.artifacts.events.GroupUpdate; +import org.spongepowered.downloads.artifacts.server.cmd.group.state.EmptyState; +import org.spongepowered.downloads.artifacts.server.cmd.group.state.GroupState; +import org.spongepowered.downloads.artifacts.server.cmd.group.state.PopulatedState; + +import java.net.URI; +import java.net.URL; + +public class GroupEntity + extends EventSourcedBehaviorWithEnforcedReplies { + + public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create(GroupCommand.class, "GroupEntity"); + private final String groupId; + + private GroupEntity(EntityContext context) { + super( + // PersistenceId needs a typeHint (or namespace) and entityId, + // we take then from the EntityContext + PersistenceId.of( + context.getEntityTypeKey().name(), // <- type hint + context.getEntityId() // <- business id + )); + // we keep a copy of cartI + this.groupId = context.getEntityId(); + + } + + public static GroupEntity create(EntityContext context) { + return new GroupEntity(context); + } + + @Override + public GroupState emptyState() { + return new EmptyState(); + } + + @Override + public EventHandler eventHandler() { + final EventHandlerBuilder builder = this.newEventHandlerBuilder(); + + builder.forState(GroupState::isEmpty) + .onEvent( + GroupUpdate.GroupRegistered.class, + this::handleRegistration + ).onEvent( + GroupUpdate.ArtifactRegistered.class, + (state, event) -> { + throw new IllegalStateException("Cannot register artifact on empty group"); + } + ); + builder.forStateType(PopulatedState.class) + .onEvent(GroupUpdate.ArtifactRegistered.class, this::handleArtifactRegistration); + + return builder.build(); + } + + private GroupState handleRegistration( + final GroupState state, final GroupUpdate.GroupRegistered event + ) { + return new PopulatedState(event.groupId(), event.name(), event.website(), HashSet.empty()); + } + + private GroupState handleArtifactRegistration( + final PopulatedState state, final GroupUpdate.ArtifactRegistered event + ) { + final var add = state.artifacts().add(event.coordinates().artifactId()); + return new PopulatedState(state.groupCoordinates(), state.name(), state.website(), add); + } + + @Override + public CommandHandlerWithReply commandHandler() { + final CommandHandlerWithReplyBuilder builder = this.newCommandHandlerWithReplyBuilder(); + + builder.forState(GroupState::isEmpty) + .onCommand(GroupCommand.RegisterGroup.class, this::respondToRegisterGroup) + .onCommand(GroupCommand.RegisterArtifact.class, (state, cmd) -> this.Effect().reply(cmd.replyTo(), new Response.GroupMissing(state.name()))) + .onCommand(GroupCommand.GetGroup.class, (cmd) -> this.Effect().reply( + cmd.replyTo(), new GroupResponse.Missing(cmd.groupId()))) + .onCommand(GroupCommand.GetArtifacts.class, (cmd) -> this.Effect().reply( + cmd.replyTo(), new GetArtifactsResponse.GroupMissing(cmd.groupId()))) + ; + builder.forStateType(PopulatedState.class) + .onCommand(GroupCommand.RegisterGroup.class, (cmd) -> this.Effect().reply( + cmd.replyTo(), new GroupRegistration.Response.GroupAlreadyRegistered(cmd.mavenCoordinates()))) + .onCommand(GroupCommand.RegisterArtifact.class, this::respondToRegisterArtifact) + .onCommand(GroupCommand.GetGroup.class, this::respondToGetGroup) + .onCommand(GroupCommand.GetArtifacts.class, this::respondToGetVersions); + builder.forNullState() + .onCommand(GroupCommand.RegisterGroup.class, this::respondToRegisterGroup) + .onCommand(GroupCommand.RegisterArtifact.class, (state, cmd) -> this.Effect().reply(cmd.replyTo(), new Response.GroupMissing(state.name()))) + .onCommand(GroupCommand.GetGroup.class, (cmd) -> this.Effect().reply( + cmd.replyTo(), new GroupResponse.Missing(cmd.groupId()))) + .onCommand(GroupCommand.GetArtifacts.class, (cmd) -> this.Effect().reply( + cmd.replyTo(), new GetArtifactsResponse.GroupMissing(cmd.groupId()))) + ; + return builder.build(); + } + + @Override + public RetentionCriteria retentionCriteria() { + return RetentionCriteria.snapshotEvery(1, 2); + } + + private ReplyEffect respondToRegisterGroup( + final GroupState state, + final GroupCommand.RegisterGroup cmd + ) { + return this.Effect() + .persist(new GroupUpdate.GroupRegistered(cmd.mavenCoordinates(), cmd.name(), cmd.website())) + .thenReply( + cmd.replyTo(), + newState -> new GroupRegistration.Response.GroupRegistered( + new Group( + newState.groupCoordinates(), + newState.name(), + newState.website() + )) + ); + } + + private ReplyEffect respondToRegisterArtifact( + final PopulatedState state, + final GroupCommand.RegisterArtifact cmd + ) { + if (state.artifacts().contains(cmd.artifact())) { + this.Effect().reply(cmd.replyTo(), new Response.ArtifactAlreadyRegistered( + cmd.artifact(), + state.groupCoordinates() + )); + } + + final var group = state.asGroup(); + final var coordinates = new ArtifactCoordinates(group.groupCoordinates(), cmd.artifact()); + final EffectFactories effect = this.Effect(); + return effect.persist(new GroupUpdate.ArtifactRegistered(new ArtifactCoordinates(state.groupCoordinates(), cmd.artifact()))) + .thenReply(cmd.replyTo(), (s) -> new Response.ArtifactRegistered(coordinates)); + } + + private ReplyEffect respondToGetGroup( + final PopulatedState state, final GroupCommand.GetGroup cmd + ) { + final String website = state.website(); + return this.Effect().reply(cmd.replyTo(), Try.of(() -> URI.create(website)) + .mapTry(URI::toURL) + .mapTry(_ -> { + final Group group = new Group(state.groupCoordinates(), state.name(), website); + return new GroupResponse.Available(group); + }) + .getOrElseGet(_ -> new GroupResponse.Missing(cmd.groupId()))); + } + + private ReplyEffect respondToGetVersions( + final PopulatedState state, + final GroupCommand.GetArtifacts cmd + ) { + return this.Effect().reply(cmd.replyTo(), new GetArtifactsResponse.ArtifactsAvailable(state.artifacts().toList().asJava())); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/EmptyState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/EmptyState.java new file mode 100644 index 00000000..deac00d0 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/EmptyState.java @@ -0,0 +1,54 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.server.cmd.group.state; + +import org.spongepowered.downloads.artifact.api.Group; + +public final class EmptyState implements GroupState { + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Group asGroup() { + return new Group("", "", ""); + } + + @Override + public String website() { + return "null"; + } + + @Override + public String name() { + return "null"; + } + + @Override + public String groupCoordinates() { + return "null"; + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/GroupState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/GroupState.java new file mode 100644 index 00000000..fd1de96a --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/GroupState.java @@ -0,0 +1,40 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.server.cmd.group.state; + +import org.spongepowered.downloads.artifact.api.Group; + +public sealed interface GroupState permits EmptyState, PopulatedState { + + boolean isEmpty(); + + Group asGroup(); + + String website(); + + String name(); + + String groupCoordinates(); +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/PopulatedState.java new file mode 100644 index 00000000..bab56100 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/PopulatedState.java @@ -0,0 +1,53 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.server.cmd.group.state; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.vavr.collection.Set; +import org.spongepowered.downloads.akka.AkkaSerializable; +import org.spongepowered.downloads.artifact.api.Group; + + +@JsonDeserialize +public record PopulatedState( + String groupCoordinates, + String name, + String website, + Set artifacts +) + implements GroupState, AkkaSerializable { + @JsonCreator + public PopulatedState { + } + + public boolean isEmpty() { + return this.groupCoordinates.isEmpty() || this.name.isEmpty(); + } + + public Group asGroup() { + return new Group(this.groupCoordinates, this.name, this.website); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/ArtifactCommandController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/ArtifactCommandController.java new file mode 100644 index 00000000..68650b5a --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/ArtifactCommandController.java @@ -0,0 +1,168 @@ +package org.spongepowered.downloads.artifacts.server.cmd.transport; + +import akka.NotUsed; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.Entity; +import akka.cluster.sharding.typed.javadsl.EntityRef; +import akka.persistence.typed.PersistenceId; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Patch; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import io.vavr.control.Either; +import jakarta.inject.Inject; +import jakarta.validation.constraints.NotNull; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.mutation.Update; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; +import org.spongepowered.downloads.artifact.api.registration.ArtifactRegistration; +import org.spongepowered.downloads.artifact.api.registration.Response; +import org.spongepowered.downloads.artifacts.server.cmd.details.ArtifactDetailsEntity; +import org.spongepowered.downloads.artifacts.server.cmd.details.DetailsCommand; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupCommand; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupEntity; + +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Controller("/groups/{groupID}/artifacts") +@Requires("command") +public class ArtifactCommandController { + + private final ClusterSharding sharding; + private final Duration askTimeout = Duration.ofSeconds(30); + + @Inject + public ArtifactCommandController( + final ClusterSharding sharding + ) { + this.sharding = sharding; + sharding.init( + Entity.of( + ArtifactDetailsEntity.ENTITY_TYPE_KEY, + ctx -> ArtifactDetailsEntity.create( + ctx, ctx.getEntityId(), PersistenceId.of(ctx.getEntityTypeKey().name(), ctx.getEntityId()) + ) + ) + ); + sharding.init( + Entity.of( + GroupEntity.ENTITY_TYPE_KEY, + GroupEntity::create + ) + ); + } + + @Post(value = "/") + public CompletionStage> registerArtifact( + @PathVariable @NotNull final String groupID, + @Body @NotNull final ArtifactRegistration.RegisterArtifact registration + ) { + final var groupEntity = this.getGroupEntity(groupID.toLowerCase(Locale.ROOT)); + final var artifactId = registration.artifactId(); + final var registerArtifact = groupEntity + .ask(replyTo1 -> + new GroupCommand.RegisterArtifact(artifactId, replyTo1), this.askTimeout + ); + return registerArtifact + .thenComposeAsync(response -> switch (response) { + case Response.ArtifactRegistered registered -> this.initializeArtifact(registration, registered) + .thenApply(_ -> HttpResponse.created(response)); + case Response.ArtifactAlreadyRegistered already -> + CompletableFuture.completedStage(HttpResponse.ok(already)); + case Response.GroupMissing missing -> CompletableFuture.completedStage(HttpResponse.notFound(missing)); + }); + } + + private CompletionStage initializeArtifact( + ArtifactRegistration.RegisterArtifact registration, Response.ArtifactRegistered registered + ) { + return this.getDetailsEntity( + registered.groupId(), registered.artifactId()) + .ask( + replyTo -> new DetailsCommand.RegisterArtifact( + registered.coordinates(), registration.displayName(), replyTo), this.askTimeout); + } + + @Patch("/{artifactID}/update") + public CompletionStage> updateDetails( + @PathVariable String groupID, + @PathVariable String artifactID, + @Body final Update update + ) { + final var coords = new ArtifactCoordinates(groupID, artifactID); + groupID = groupID.toLowerCase(Locale.ROOT); + artifactID = artifactID.toLowerCase(Locale.ROOT); + return switch (update) { + case Update.Website w -> { + final var validate = w.validate(); + if (validate.isLeft()) { + yield validate.>mapLeft(HttpResponse::badRequest) + .mapLeft(CompletableFuture::completedFuture) + .getLeft(); + } + final var validUrl = validate.get(); + final var response = this.getDetailsEntity(groupID, artifactID) + .>ask( + r -> new DetailsCommand.UpdateWebsite(coords, validUrl, r), this.askTimeout); + yield response.thenApply(e -> e.fold(HttpResponse::notFound, HttpResponse::created)); + } + case Update.DisplayName d -> { + final var validate = d.validate(); + if (validate.isLeft()) { + yield validate.>mapLeft(HttpResponse::badRequest) + .mapLeft(CompletableFuture::completedFuture) + .getLeft(); + } + final var displayName = validate.get(); + final var response = this.getDetailsEntity(groupID, artifactID) + .>ask( + r -> new DetailsCommand.UpdateDisplayName(coords, displayName, r), this.askTimeout); + yield response.thenApply(e -> e.fold(HttpResponse::notFound, HttpResponse::created)); + } + case Update.GitRepository gr -> { + final var validate = gr.validate(); + if (validate.isLeft()) { + yield validate.>mapLeft(HttpResponse::badRequest) + .mapLeft(CompletableFuture::completedFuture) + .getLeft(); + } + + final var response = this.getDetailsEntity(groupID, artifactID) + .>ask( + r -> new DetailsCommand.UpdateGitRepository(coords, gr.gitRepo(), r), this.askTimeout); + yield response.thenApply(e -> e.fold(HttpResponse::badRequest, HttpResponse::created)); + } + case Update.Issues i -> { + final var validate = i.validate(); + if (validate.isLeft()) { + yield validate.>mapLeft(HttpResponse::badRequest) + .mapLeft(CompletableFuture::completedFuture) + .getLeft(); + } + final var validUrl = validate.get(); + final var response = this.getDetailsEntity(groupID, artifactID) + .>ask( + r -> new DetailsCommand.UpdateIssues(coords, validUrl, r), this.askTimeout); + yield response.thenApply(e -> e.fold(HttpResponse::badRequest, HttpResponse::created)); + } + }; + } + + private EntityRef getGroupEntity(final String groupId) { + return this.sharding.entityRefFor(GroupEntity.ENTITY_TYPE_KEY, groupId.toLowerCase(Locale.ROOT)); + } + + private EntityRef getDetailsEntity(final String groupId, final String artifactId) { + return this.sharding.entityRefFor( + ArtifactDetailsEntity.ENTITY_TYPE_KEY, + STR."\{groupId.toLowerCase(Locale.ROOT)}:\{artifactId.toLowerCase(Locale.ROOT)}" + ); + } + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitRemoteValidationException.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitRemoteValidationException.java new file mode 100644 index 00000000..6075e410 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitRemoteValidationException.java @@ -0,0 +1,9 @@ +package org.spongepowered.downloads.artifacts.server.lib.git; + +public class GitRemoteValidationException extends Throwable { + public GitRemoteValidationException(final String message) { + super(message); + } + + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitResolver.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitResolver.java new file mode 100644 index 00000000..310260de --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/lib/git/GitResolver.java @@ -0,0 +1,66 @@ +package org.spongepowered.downloads.artifacts.server.lib.git; + +import io.vavr.control.Either; +import io.vavr.control.Try; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +@Singleton +public class GitResolver { + + @Inject + @Named("git-resolver") + private ExecutorService executorService; + + public CompletableFuture> validateRepository( + final String repoURL + ) { + final var gitLs = CompletableFuture.supplyAsync(() -> Try.of(() -> Git.lsRemoteRepository() + .setRemote(repoURL) + .setTags(false) + .setHeads(false) + .setTimeout(60) + .call()) + .toEither() + .mapLeft(t -> switch (t) { + case InvalidRemoteException _ -> this.invalidRemote(repoURL); + case GitAPIException e -> this.genericRemoteProblem(e); + default -> this.badRequest(repoURL, t); + }), this.executorService); + final var validatedReferences = gitLs.thenApply(e -> e.map(refs -> !refs.isEmpty()) + .flatMap(valid -> { + if (!valid) { + return Either.left(this.noReferences(repoURL)); + } + return Either.right(repoURL); + })); + return validatedReferences.toCompletableFuture(); + } + + private ArtifactDetails.Response noReferences(String repoUrl) { + return new ArtifactDetails.Response.Error(String.format("Invalid remote: %s. No references found", repoUrl)); + } + + private ArtifactDetails.Response badRequest(String repoURL, Throwable t) { + return new ArtifactDetails.Response.Error(String.format("Invalid remote: %s. got error %s", repoURL, t)); + } + + private ArtifactDetails.Response invalidRemote(String repoURL) { + return new ArtifactDetails.Response.Error(String.format("Invalid remote: %s", repoURL)); + } + + private ArtifactDetails.Response genericRemoteProblem(GitAPIException t) { + return new ArtifactDetails.Response.Error(String.format("Error resolving repository: %s", t)); + } + +} + + diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java deleted file mode 100644 index ed8de22f..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.query.group.domain; - -import io.micronaut.data.annotation.GeneratedValue; -import io.micronaut.data.annotation.Id; -import io.micronaut.data.annotation.MappedEntity; -import io.micronaut.data.annotation.MappedProperty; -import io.micronaut.data.annotation.Relation; -import io.micronaut.serde.annotation.Serdeable; -import org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact; - -import java.util.List; - -@MappedEntity(value = "groups") -@Serdeable -public class GroupOrg { - - @Id - @GeneratedValue - private int id; - - @MappedProperty(value = "groupId") - private String groupId; - - @Relation(value =Relation.Kind.ONE_TO_MANY, mappedBy = "groupId") - private List artifacts; - -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java deleted file mode 100644 index 4ebb194d..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.query.meta; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -import java.io.Serializable; -import java.util.Set; - -/** - * DTO for {@link org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact} - */ -public record ArtifactDto( - @NotNull String groupId, - @NotEmpty String artifactId, - String displayName, - String website, - String gitRepo, - String issues, - @NotNull Set tagValues -) implements Serializable { - /** - * DTO for {@link org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifactTagValue} - */ - public record Tag( - String artifactId, - String groupId, - String tagName, - String tagValue - ) implements Serializable { - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java index 5032a032..14fafbd9 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java @@ -1,21 +1,18 @@ package org.spongepowered.downloads.artifacts.server.query.meta; import io.micronaut.core.annotation.NonNull; -import io.micronaut.data.annotation.Query; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.r2dbc.annotation.R2dbcRepository; import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository; import org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.List; -import java.util.Optional; - @R2dbcRepository(dialect = Dialect.POSTGRES) public interface ArtifactRepository extends ReactiveStreamsCrudRepository { @NonNull - List findArtifactIdByGroupId(@NonNull String groupId); + Flux findArtifactIdByGroupId(@NonNull String groupId); @NonNull Mono findByGroupIdAndArtifactId(String groupId, String artifactId); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java index 05741eba..1ca7c09e 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java @@ -11,17 +11,15 @@ @Serdeable @MappedEntity -public class Group { - +public record Group( @GeneratedValue @Id - private Long id; - + Long id, @MappedProperty(value = "groupId") - private String groupId; - + String groupId, @Relation(value = Relation.Kind.ONE_TO_MANY, mappedBy = "groupId") - private List artifacts; + List artifacts +) { } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java index 27c5456a..2b74aee9 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java @@ -27,101 +27,72 @@ import io.micronaut.data.annotation.GeneratedValue; import io.micronaut.data.annotation.Id; -import io.micronaut.data.annotation.Join; import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.MappedProperty; -import io.micronaut.data.annotation.Relation; -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.collection.Map; -import io.vavr.collection.SortedSet; -import io.vavr.collection.TreeMap; -import io.vavr.collection.TreeSet; +import io.micronaut.data.annotation.Transient; +import io.micronaut.data.annotation.sql.JoinColumn; +import io.micronaut.data.annotation.sql.JoinColumns; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotEmpty; -import org.apache.maven.artifact.versioning.ComparableVersion; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import java.util.Comparator; +import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; -@MappedEntity(value = "artifacts", schema = "artifacts", alias = "artifact") -public class JpaArtifact { +@MappedEntity(value = "artifacts", schema = "artifact", alias = "artifact") +public record JpaArtifact( @GeneratedValue @Id - private int id; - - public int getId() { - return id; - } - + int id, @MappedProperty(value = "group_id") - private String groupId; - + String groupId, @NotEmpty @MappedProperty(value = "artifact_id") - private String artifactId; - + String artifactId, @MappedProperty(value = "display_name") - private String displayName; - + String displayName, @MappedProperty(value = "website") - private String website; - + String website, @MappedProperty(value = "git_repository") - private String gitRepo; - + String gitRepo, @MappedProperty(value = "issues") - private String issues; - - private Set tagValues; - - public String getGroupId() { - return groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public String getDisplayName() { - return displayName; - } - - public String getWebsite() { - return website; - } - - public String getGitRepo() { - return gitRepo; - } - - public String getIssues() { - return issues; - } - - public Set getTagValues() { - return tagValues; - } - - public ArtifactCoordinates getCoordinates() { + String issues, + + @OneToMany(fetch = FetchType.EAGER, targetEntity = JpaArtifactTagValue.class) + @JoinColumns({ + @JoinColumn(name = "artifact_id", + referencedColumnName = "artifact_id"), + @JoinColumn(name = "group_id", + referencedColumnName = "group_id") + }) + Set tagValues +) { + + @Transient + public ArtifactCoordinates coordinates() { return new ArtifactCoordinates(this.groupId, this.artifactId); } - public Map> getTagValuesForReply() { - final var tagValues = this.getTagValues(); - final var tagTuples = tagValues.stream() - .map(value -> Tuple.of(value.getTagName(), value.getTagValue())) - .toList(); + @Transient + public Map> tags() { + return this.tagValues.stream() + .collect( + Collectors.groupingBy( + JpaArtifactTagValue::getTagName, + Collectors.mapping( + JpaArtifactTagValue::getTagValue, + Collectors.toCollection(TreeSet::new) + ) + ) + ); + } - var versionedTags = TreeMap.>empty(); - final var comparator = Comparator.comparing(ComparableVersion::new).reversed(); - for (final Tuple2 tagged : tagTuples) { - versionedTags = versionedTags.put(tagged._1, TreeSet.of(comparator, tagged._2), SortedSet::addAll); - } - return versionedTags; - } } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java index 9411baf9..20359e9b 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java @@ -26,11 +26,16 @@ import io.micronaut.data.annotation.MappedEntity; import io.micronaut.data.annotation.MappedProperty; +import io.micronaut.data.annotation.sql.JoinColumn; +import io.micronaut.data.annotation.sql.JoinColumns; +import jakarta.persistence.IdClass; +import jakarta.persistence.ManyToOne; import java.io.Serializable; import java.util.Objects; -@MappedEntity(value = "versioned_tags", schema = "version") +@IdClass(JpaArtifactTagValue.Identifier.class) +@MappedEntity(value = "artifact_tag_values", schema = "artifact") public class JpaArtifactTagValue { /* @@ -66,6 +71,11 @@ public int hashCode() { } } + @ManyToOne(fetch = jakarta.persistence.FetchType.LAZY, targetEntity = JpaArtifact.class) + @JoinColumns({ + @JoinColumn(name = "artifact_id", referencedColumnName = "artifact_id"), + @JoinColumn(name = "group_id", referencedColumnName = "group_id") + }) private JpaArtifact artifact; @MappedProperty(value = "artifact_id") diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java similarity index 54% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java index 87c90b5b..eb5e18ce 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java @@ -1,4 +1,4 @@ -package org.spongepowered.downloads.artifacts.server.query.meta; +package org.spongepowered.downloads.artifacts.server.query.transport; import akka.actor.typed.ActorSystem; import akka.actor.typed.SpawnProtocol; @@ -11,11 +11,12 @@ import io.micronaut.http.annotation.PathVariable; import io.micronaut.http.annotation.Status; import jakarta.inject.Inject; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.query.GetArtifactDetailsResponse; import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; +import org.spongepowered.downloads.artifacts.server.query.meta.ArtifactRepository; import reactor.core.publisher.Mono; -import java.util.List; - @Controller("/groups/{groupID}/artifacts") @Requires("query") public class ArtifactQueryController { @@ -37,16 +38,41 @@ public ArtifactQueryController( this.artifactsRepo = artifactsRepo; } + @Get(value = "/", produces = MediaType.APPLICATION_JSON) + @Status(HttpStatus.OK) + public Mono getArtifacts( + final @PathVariable String groupID + ) { + return this.artifactsRepo.findArtifactIdByGroupId(groupID) + .collectList() + .map(GetArtifactsResponse.ArtifactsAvailable::new) + .onErrorReturn(new GetArtifactsResponse.GroupMissing(groupID)); + } + + /** + * Get the details of an artifact. + * + * @param groupID The group ID of the artifact + * @param artifactId The artifact ID of the artifact + * @return The details of the artifact + */ @Get(value = "/{artifactId}", produces = MediaType.APPLICATION_JSON ) @Status(HttpStatus.OK) - public Mono getArtifacts( + public Mono getArtifact( final @PathVariable String groupID, final @PathVariable String artifactId ) { return this.artifactsRepo.findByGroupIdAndArtifactId(groupID, artifactId) - .map(a -> new GetArtifactsResponse.ArtifactsAvailable(List.of(a.getArtifactId()))) - .onErrorReturn(new GetArtifactsResponse.GroupMissing(groupID)); + .map(a -> new GetArtifactDetailsResponse.RetrievedArtifact( + a.coordinates(), + a.displayName(), + a.website(), + a.gitRepo(), + a.issues(), + a.tags() + )) + .onErrorReturn(new GetArtifactDetailsResponse.MissingArtifact(new ArtifactCoordinates(groupID, artifactId))); } } diff --git a/artifacts/server/src/main/resources/application.conf b/artifacts/server/src/main/resources/application.conf index 997ab44f..8c111f48 100644 --- a/artifacts/server/src/main/resources/application.conf +++ b/artifacts/server/src/main/resources/application.conf @@ -2,6 +2,7 @@ akka.persistence.journal.plugin = "akka.persistence.r2dbc.journal" akka.persistence.snapshot-store.plugin = "akka.persistence.r2dbc.snapshot" akka.persistence.state.plugin = "akka.persistence.r2dbc.state" +akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.postgres} akka.persistence.r2dbc { journal.payload-column-type = JSONB snapshot.payload-column-type = JSONB @@ -9,11 +10,16 @@ akka.persistence.r2dbc { } akka.serialization.jackson.jackson-json.compression.algorithm = off +akka { + actor { + serialization-bindings { + "org.spongepowered.downloads.akka.AkkaSerializable" = jackson-json + } + } +} akka.persistence.r2dbc { - dialect = "postgres" connection-factory { - driver = "postgres" host = "localhost" host = ${?DB_HOST} database = "default" diff --git a/artifacts/server/src/main/resources/application.yaml b/artifacts/server/src/main/resources/application.yaml index 754ed774..d5ed2675 100644 --- a/artifacts/server/src/main/resources/application.yaml +++ b/artifacts/server/src/main/resources/application.yaml @@ -22,3 +22,11 @@ liquibase: default: enabled: true change-log: 'classpath:db/liquibase-changelog.xml' # (4) +micronaut: + executors: + git-resolver: + core-pool-size: 2 + max-pool-size: 10 + keep-alive-time: 30s + queue-size: 1000 + thread-factory: git-resolver diff --git a/artifacts/server/src/main/resources/db/akka/akka_001_init.sql b/artifacts/server/src/main/resources/db/akka/akka_001_init.sql new file mode 100644 index 00000000..045c8cbc --- /dev/null +++ b/artifacts/server/src/main/resources/db/akka/akka_001_init.sql @@ -0,0 +1,62 @@ +CREATE TABLE IF NOT EXISTS event_journal +( + slice INT NOT NULL, + entity_type VARCHAR(255) NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + seq_nr BIGINT NOT NULL, + db_timestamp timestamp with time zone NOT NULL, + + event_ser_id INTEGER NOT NULL, + event_ser_manifest VARCHAR(255) NOT NULL, + event_payload JSONB NOT NULL, + + deleted BOOLEAN DEFAULT FALSE NOT NULL, + writer VARCHAR(255) NOT NULL, + adapter_manifest VARCHAR(255), + tags TEXT ARRAY, + + meta_ser_id INTEGER, + meta_ser_manifest VARCHAR(255), + meta_payload BYTEA, + + PRIMARY KEY (persistence_id, seq_nr) +); + +-- `event_journal_slice_idx` is only needed if the slice based queries are used +CREATE INDEX IF NOT EXISTS event_journal_slice_idx ON event_journal (slice, entity_type, db_timestamp, seq_nr); + +CREATE TABLE IF NOT EXISTS snapshot +( + slice INT NOT NULL, + entity_type VARCHAR(255) NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + seq_nr BIGINT NOT NULL, + write_timestamp BIGINT NOT NULL, + ser_id INTEGER NOT NULL, + ser_manifest VARCHAR(255) NOT NULL, + snapshot JSONB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest VARCHAR(255), + meta_payload BYTEA, + + PRIMARY KEY (persistence_id) +); + +CREATE TABLE IF NOT EXISTS durable_state +( + slice INT NOT NULL, + entity_type VARCHAR(255) NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + revision BIGINT NOT NULL, + db_timestamp timestamp with time zone NOT NULL, + + state_ser_id INTEGER NOT NULL, + state_ser_manifest VARCHAR(255), + state_payload JSONB NOT NULL, + tags TEXT ARRAY, + + PRIMARY KEY (persistence_id, revision) +); + +-- `durable_state_slice_idx` is only needed if the slice based queries are used +CREATE INDEX IF NOT EXISTS durable_state_slice_idx ON durable_state (slice, entity_type, db_timestamp, revision); diff --git a/artifacts/server/src/main/resources/db/akka/akka_2_8_2.xml b/artifacts/server/src/main/resources/db/akka/akka_2_8_2.xml new file mode 100644 index 00000000..a42d4c88 --- /dev/null +++ b/artifacts/server/src/main/resources/db/akka/akka_2_8_2.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/artifacts/server/src/main/resources/db/changelog/01-create-artifacts-schema.xml b/artifacts/server/src/main/resources/db/changelog/01-create-artifacts-schema.xml index b6adedcb..073f7577 100644 --- a/artifacts/server/src/main/resources/db/changelog/01-create-artifacts-schema.xml +++ b/artifacts/server/src/main/resources/db/changelog/01-create-artifacts-schema.xml @@ -1,5 +1,3 @@ - - - CREATE SCHEMA IF NOT EXISTS artifacts; + CREATE SCHEMA IF NOT EXISTS artifact; @@ -56,7 +54,7 @@ + + + @@ -89,36 +90,52 @@ type="boolean" defaultValueBoolean="false"/> - + select distinct a.artifact_id, a.group_id, v.version, v.recommended, v.manual_recommendation from artifact.artifacts a inner join artifact.artifact_versions v on a.id = v.artifact_id - - select - a.group_id, - a.artifact_id, - av.version, - va.classifier, va.extension, va.download_url, va.md5, va.sha1 - from artifact.versioned_assets va - inner join artifact.artifact_versions av on av.id = va.version_id - inner join artifact.artifacts a on a.id = av.artifact_id - - - select distinct a.group_id, a.artifact_id, v.version, v.ordering, v.id as version_id, vc.commit_sha, vc.repo, vc.branch, vc.changelog - from artifact.version_changelogs vc - inner join artifact.artifact_versions v on v.id = vc.version_id - inner join artifact.artifacts a on a.id = v.artifact_id - order by v.ordering desc - - - select distinct a.artifact_id, a.group_id, v.version, v.ordering, v.recommended, v.manual_recommendation - from artifact.artifacts a inner join artifact.artifact_versions v on a.id = v.artifact_id - order by v.ordering desc + + set search_path to artifact; +drop materialized view if exists versioned_tags; +create materialized view versioned_tags as + select version.id as version_id, + a.id as artifact_internal_id, + a.group_id as maven_group_id, + a.artifact_id as maven_artifact_id, + version.version as maven_version, + version.recommended as regex_recommended, + artifact_tag.id as tag_id, + artifact_tag.tag_name as tag_name, + ((regexp_match(version.version, artifact_tag.tag_regex))[artifact_tag.use_capture_group]) as tag_value + from artifact_versions version + inner join artifacts a on version.artifact_id = a.id + inner join artifact_tags artifact_tag + on a.id = artifact_tag.artifact_id + ; + create unique index on versioned_tags ( + maven_group_id, maven_artifact_id, + maven_version, tag_name + ); + + + + + + + + + + + + + + + + select distinct a.group_id, a.artifact_id, t.tag_name, vt.tag_value + from artifact.artifacts a + inner join artifact.artifact_tags t on a.id = t.artifact_id + inner join artifact.versioned_tags vt on vt.tag_name = t.tag_name and vt.maven_group_id = a.group_id and + vt.maven_artifact_id = a.artifact_id and vt.tag_value is not null diff --git a/artifacts/server/src/main/resources/db/liquibase-changelog.xml b/artifacts/server/src/main/resources/db/liquibase-changelog.xml index 92a9682b..3d53d5b5 100644 --- a/artifacts/server/src/main/resources/db/liquibase-changelog.xml +++ b/artifacts/server/src/main/resources/db/liquibase-changelog.xml @@ -4,7 +4,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> - - + + diff --git a/artifacts/server/src/main/resources/logback.xml b/artifacts/server/src/main/resources/logback.xml index 8cb499db..c4284b98 100644 --- a/artifacts/server/src/main/resources/logback.xml +++ b/artifacts/server/src/main/resources/logback.xml @@ -13,8 +13,4 @@ - - - - diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ApplicationTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ApplicationTest.java new file mode 100644 index 00000000..d6712e3e --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ApplicationTest.java @@ -0,0 +1,29 @@ +package org.spongepowered.downloads.test.artifacts.server; + +import akka.actor.typed.ActorSystem; +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@MicronautTest +public class ApplicationTest { + + + @Inject + EmbeddedApplication application; + @Inject + ActorSystem system; + private final Logger logger = LoggerFactory.getLogger("ArtifactRepositoryTest"); + + @Test + public void testItWorks() { + Assertions.assertTrue(application.isRunning()); + Assertions.assertNotNull(system); + final String msg = system.printTree(); + logger.info(msg); + } +} diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java index 8a5c796d..8e3d01e8 100644 --- a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java @@ -1,30 +1,24 @@ package org.spongepowered.downloads.test.artifacts.server; import io.micronaut.context.BeanContext; -import io.micronaut.core.type.Argument; import io.micronaut.data.annotation.Query; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.client.BlockingHttpClient; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.runtime.EmbeddedApplication; -import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; import jakarta.inject.Inject; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; import org.spongepowered.downloads.artifacts.server.query.meta.ArtifactRepository; import org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact; import reactor.core.publisher.Mono; -import java.util.List; -import java.util.Optional; +import java.util.Collections; @MicronautTest @@ -37,58 +31,14 @@ public class ArtifactRepositoryTest { @Inject EmbeddedApplication application; - @Inject - @Client("/") - HttpClient httpClient; + + private final Logger logger = LoggerFactory.getLogger("ArtifactRepositoryTest"); @Test public void testItWorks() { Assertions.assertTrue(application.isRunning()); } - @Test - void migrationsAreExposedViaAndEndpoint() { - BlockingHttpClient client = httpClient.toBlocking(); - - HttpResponse> response = client.exchange( - HttpRequest.GET("/liquibase"), - Argument.listOf(LiquibaseReport.class) - ); - Assertions.assertEquals(HttpStatus.OK, response.status()); - - LiquibaseReport liquibaseReport = response.body().get(0); - Assertions.assertNotNull(liquibaseReport); - Assertions.assertNotNull(liquibaseReport.getChangeSets()); - Assertions.assertEquals(2, liquibaseReport.getChangeSets().size()); - } - @Serdeable - static class LiquibaseReport { - - private List changeSets; - - public void setChangeSets(List changeSets) { - this.changeSets = changeSets; - } - - public List getChangeSets() { - return changeSets; - } - } - - @Serdeable - static class ChangeSet { - - private String id; - - public void setId(String id) { - this.id = id; - } - - public String getId() { - return id; - } - } - @Test public void testAnnotation() { final BeanDefinition beanDefinition = context.getBeanDefinition(ArtifactRepository.class); @@ -98,7 +48,7 @@ public void testAnnotation() { .getAnnotationMetadata().stringValue(Query.class) // (3) .orElse(null); - final String expected = "SELECT artifact.\"id\",artifact.\"group_id\",artifact.\"artifact_id\",artifact.\"display_name\",artifact.\"website\",artifact.\"git_repository\",artifact.\"issues\",artifact.\"tag_values\",artifact.\"coordinates\",artifact.\"tag_values_for_reply\" FROM \"artifacts\".\"artifacts\" artifact WHERE (artifact.\"group_id\" = $1 AND artifact.\"artifact_id\" = $2)"; + final String expected = "SELECT artifact.\"id\",artifact.\"group_id\",artifact.\"artifact_id\",artifact.\"display_name\",artifact.\"website\",artifact.\"git_repository\",artifact.\"issues\" FROM \"artifact\".\"artifacts\" artifact WHERE (artifact.\"group_id\" = $1 AND artifact.\"artifact_id\" = $2)"; Assertions.assertEquals( // (4) expected, query); } @@ -107,10 +57,17 @@ public void testAnnotation() { public void testGetArtifact() { final ArtifactRepository repo = context.createBean(ArtifactRepository.class); - - final Mono spongevanilla = repo.findByGroupIdAndArtifactId("org.spongepowered", "spongevanilla"); + // Example is injected with test data through liquibase resources + final Mono spongevanilla = repo.findByGroupIdAndArtifactId("com.example", "example"); final JpaArtifact a = spongevanilla.block(); + + final var expected = new ArtifactCoordinates("com.example", "example"); + Assertions.assertNotNull(a); + Assertions.assertEquals(expected, a.coordinates()); + final var expectedTags = Collections.emptyMap(); + Assertions.assertNotNull(a.tags()); + Assertions.assertEquals(expectedTags, a.tags()); } diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/LiquibaseTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/LiquibaseTest.java new file mode 100644 index 00000000..886b146e --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/LiquibaseTest.java @@ -0,0 +1,53 @@ +package org.spongepowered.downloads.test.artifacts.server; + +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.List; + +@MicronautTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestResourcesScope("testcontainers") +public class LiquibaseTest { + + @Inject + @Client("/") + HttpClient httpClient; + + @Test + void migrationsAreExposedViaAndEndpoint() { + BlockingHttpClient client = httpClient.toBlocking(); + + HttpResponse> response = client.exchange( + HttpRequest.GET("/liquibase"), + Argument.listOf(LiquibaseReport.class) + ); + Assertions.assertEquals(HttpStatus.OK, response.status()); + + LiquibaseReport liquibaseReport = response.body().get(0); + Assertions.assertNotNull(liquibaseReport); + Assertions.assertNotNull(liquibaseReport.changeSets()); + Assertions.assertEquals(3, liquibaseReport.changeSets().size()); + } + @Serdeable + record LiquibaseReport(List changeSets) { + + } + + @Serdeable + record ChangeSet( String id) { + } + +} diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/ArtifactDetailsTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/ArtifactDetailsTest.java new file mode 100644 index 00000000..81e0d6a1 --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/ArtifactDetailsTest.java @@ -0,0 +1,72 @@ +package org.spongepowered.downloads.test.artifacts.server.cmd.details; + +import akka.NotUsed; +import akka.actor.testkit.typed.javadsl.ActorTestKit; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.EntityContext; +import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; +import akka.persistence.typed.PersistenceId; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifacts.events.DetailsEvent; +import org.spongepowered.downloads.artifacts.server.cmd.details.ArtifactDetailsEntity; +import org.spongepowered.downloads.artifacts.server.cmd.details.DetailsCommand; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.DetailsState; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.PopulatedState; +import org.testcontainers.utility.TestEnvironment; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class ArtifactDetailsTest implements BeforeEachCallback { + + private static final Config appConfig = ConfigFactory.load().withFallback(ConfigFactory.defaultApplication()); + private static final ActorTestKit testKit = ActorTestKit.create(EventSourcedBehaviorTestKit.config().withFallback(appConfig.resolve())); + + @Inject + TestEnvironment environment; + + private static final EventSourcedBehaviorTestKit behaviorTestKit = + EventSourcedBehaviorTestKit.create( + testKit.system(), + ArtifactDetailsEntity.create( + new EntityContext<>(ArtifactDetailsEntity.ENTITY_TYPE_KEY, "org.spongepowered:example", + testKit.createTestProbe().ref() + ), "org.spongepowered:example", PersistenceId.of("DetailsEntity", "org.spongepowered:example")) + ); + + @Override + public void beforeEach(final ExtensionContext context) { + behaviorTestKit.clear(); + } + + @Test + public void testAndPopulate() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var example = behaviorTestKit.runCommand( + replyTo -> new DetailsCommand.RegisterArtifact(coordinates, "Example", replyTo)); + Assertions.assertEquals(NotUsed.notUsed(), example.reply()); + // This verifies that the populated state is valid, not an empty state + Assertions.assertEquals(new PopulatedState(coordinates, "Example", "", "", ""), example.state()); + } + + @Test + public void testReRegister() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var unused = behaviorTestKit.runCommand( + replyTo -> new DetailsCommand.RegisterArtifact(coordinates, "Example", replyTo)); + final var obscure = new ArtifactCoordinates("com.example", "somethingelse"); + final var example = behaviorTestKit.runCommand( + replyTo -> new DetailsCommand.RegisterArtifact(obscure, "replaced", replyTo)); + Assertions.assertEquals(NotUsed.notUsed(), example.reply()); + Assertions.assertEquals(new PopulatedState(coordinates, "Example", "", "", ""), example.state()); + Assertions.assertEquals(0, example.events().size()); + Assertions.assertEquals(2, unused.events().size()); + } + +} diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/StateTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/StateTest.java new file mode 100644 index 00000000..8f3ad395 --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/StateTest.java @@ -0,0 +1,110 @@ +package org.spongepowered.downloads.test.artifacts.server.cmd.details; + + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifacts.events.DetailsEvent; +import org.spongepowered.downloads.artifacts.events.DetailsEvent.ArtifactGitRepositoryUpdated; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.EmptyState; +import org.spongepowered.downloads.artifacts.server.cmd.details.state.PopulatedState; + +public class StateTest { + + public static final String GIT_REPO = "https://github.com/SpongePowered/Example.git"; + public static final String EXPECTED_COORDINATES = "org.spongepowered:example"; + public static final String ISSUES_URL = "https://github.com/SpongePowered/Example/issues"; + + @Test + public void testEmptyState() { + final var state = new EmptyState(); + Assertions.assertTrue(state.isEmpty()); + Assertions.assertEquals(state.coordinates().asMavenString(), ":"); + Assertions.assertEquals(state.gitRepository(), ""); + Assertions.assertEquals(state.displayName(), ""); + Assertions.assertEquals(state.issues(), ""); + Assertions.assertEquals(state.website(), ""); + } + + @Test + public void testEmptyToPopulated() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var populated = new PopulatedState(coordinates, "Example", "", "", ""); + Assertions.assertFalse(populated.isEmpty()); + Assertions.assertEquals(populated.displayName(), "Example"); + Assertions.assertEquals(populated.coordinates().asMavenString(), EXPECTED_COORDINATES); + Assertions.assertEquals(populated.gitRepository(), ""); + Assertions.assertEquals(populated.issues(), ""); + Assertions.assertEquals(populated.website(), ""); + } + + @Test + public void testPopulatedWithGit() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var populated = new PopulatedState(coordinates, "Example", "", "", ""); + final var git = populated.withGitRepo(new ArtifactGitRepositoryUpdated(coordinates, GIT_REPO)); + Assertions.assertFalse(git.isEmpty()); + Assertions.assertEquals(git.displayName(), "Example"); + Assertions.assertEquals(git.coordinates().asMavenString(), EXPECTED_COORDINATES); + Assertions.assertEquals(git.gitRepository(), GIT_REPO); + Assertions.assertEquals(git.issues(), ""); + Assertions.assertEquals(git.website(), ""); + } + + @Test + public void testPopulatedWithGitAndIssues() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var populated = new PopulatedState(coordinates, "Example", "", "", ""); + final var git = populated.withGitRepo(new ArtifactGitRepositoryUpdated(coordinates, GIT_REPO)); + Assertions.assertInstanceOf(PopulatedState.class, git); + final var issues = ((PopulatedState) git). + withIssues(new DetailsEvent.ArtifactIssuesUpdated(coordinates, ISSUES_URL)); + Assertions.assertFalse(issues.isEmpty()); + Assertions.assertEquals(issues.displayName(), "Example"); + Assertions.assertEquals(issues.coordinates().asMavenString(), EXPECTED_COORDINATES); + Assertions.assertEquals(issues.gitRepository(), GIT_REPO); + Assertions.assertEquals(issues.issues(), ISSUES_URL); + Assertions.assertEquals(issues.website(), ""); + } + + @Test + public void testPopulatedWithGitAndIssuesAndWebsite() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var populated = new PopulatedState(coordinates, "Example", "", "", ""); + final var git = populated.withGitRepo(new ArtifactGitRepositoryUpdated(coordinates, GIT_REPO)); + Assertions.assertInstanceOf(PopulatedState.class, git); + final var issues = ((PopulatedState) git). + withIssues(new DetailsEvent.ArtifactIssuesUpdated(coordinates, ISSUES_URL)); + Assertions.assertInstanceOf(PopulatedState.class, issues); + final var website = ((PopulatedState) issues). + withWebsite(new DetailsEvent.ArtifactWebsiteUpdated(coordinates, "https://example.com")); + Assertions.assertFalse(website.isEmpty()); + Assertions.assertEquals(website.displayName(), "Example"); + Assertions.assertEquals(website.coordinates().asMavenString(), EXPECTED_COORDINATES); + Assertions.assertEquals(website.gitRepository(), GIT_REPO); + Assertions.assertEquals(website.issues(), ISSUES_URL); + Assertions.assertEquals(website.website(), "https://example.com"); + } + + @Test + public void testPopulatedWithNewDisplayName() { + final var coordinates = new ArtifactCoordinates("org.spongepowered", "example"); + final var populated = new PopulatedState(coordinates, "Example", "", "", ""); + final var git = populated.withGitRepo(new ArtifactGitRepositoryUpdated(coordinates, GIT_REPO)); + Assertions.assertInstanceOf(PopulatedState.class, git); + final var issues = ((PopulatedState) git). + withIssues(new DetailsEvent.ArtifactIssuesUpdated(coordinates, ISSUES_URL)); + Assertions.assertInstanceOf(PopulatedState.class, issues); + final var website = ((PopulatedState) issues). + withWebsite(new DetailsEvent.ArtifactWebsiteUpdated(coordinates, "https://example.com")); + Assertions.assertInstanceOf(PopulatedState.class, website); + final var displayName = ((PopulatedState) website). + withDisplayName(new DetailsEvent.ArtifactDetailsUpdated(coordinates, "New Example")); + Assertions.assertFalse(displayName.isEmpty()); + Assertions.assertEquals(displayName.displayName(), "New Example"); + Assertions.assertEquals(displayName.coordinates().asMavenString(), EXPECTED_COORDINATES); + Assertions.assertEquals(displayName.gitRepository(), GIT_REPO); + Assertions.assertEquals(displayName.issues(), ISSUES_URL); + Assertions.assertEquals(displayName.website(), "https://example.com"); + } +} diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/transport/ArtifactControllerTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/transport/ArtifactControllerTest.java new file mode 100644 index 00000000..24ea4171 --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/transport/ArtifactControllerTest.java @@ -0,0 +1,83 @@ +package org.spongepowered.downloads.test.artifacts.server.cmd.transport; + + +import akka.actor.testkit.typed.javadsl.ActorTestKit; +import akka.actor.typed.ActorSystem; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.Entity; +import akka.cluster.typed.Cluster; +import akka.cluster.typed.Join; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.registration.ArtifactRegistration; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupCommand; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupEntity; +import org.spongepowered.downloads.artifacts.server.cmd.transport.ArtifactCommandController; + +@MicronautTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestResourcesScope("testcontainers") +public class ArtifactControllerTest { + @Inject ArtifactCommandController controller; + + @Inject ActorSystem system; + + @Inject ClusterSharding sharding; + + @Inject ActorTestKit testkit; + + private static final Logger logger = LoggerFactory.getLogger("ArtifactControllerTest"); + + @BeforeAll + void init() { + System.out.println("BeforeAll"); + } + + @BeforeEach + void initCluster() { + final var cluster = Cluster.get(this.system); + cluster.manager().tell(new Join(cluster.selfMember().address())); + } + + @Test + void testRequiredArgumentsPostNewArtifact() { + Assertions.assertThrows(ConstraintViolationException.class, () -> this.controller.registerArtifact(null, null)); + } + + @Test + void testRequiredPostBodyNewArtifact() { + Assertions.assertThrows(ConstraintViolationException.class, () -> this.controller.registerArtifact("com.example", null)); + } + + @Test + void testRequiredBodyNewArtifact() { + final var probe = this.testkit.createTestProbe(GroupRegistration.Response.class); + this.sharding.init(Entity.of( + GroupEntity.ENTITY_TYPE_KEY, + GroupEntity::create + )); + + this.sharding.entityRefFor(GroupEntity.ENTITY_TYPE_KEY, "com.example").tell(new GroupCommand.RegisterGroup( + "com.example", + "example", + ",", + probe.ref() + )); + final var groupRegistrationResp = probe.expectMessageClass(GroupRegistration.Response.GroupRegistered.class); + logger.info("{} response", groupRegistrationResp); + + final var future = this.controller.registerArtifact("com.example", new ArtifactRegistration.RegisterArtifact("example", "Example")); + final var response = future.toCompletableFuture().join(); + Assertions.assertNotNull(response); + } +} diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/lib/git/GitResolverTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/lib/git/GitResolverTest.java new file mode 100644 index 00000000..e57e9c77 --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/lib/git/GitResolverTest.java @@ -0,0 +1,41 @@ +package org.spongepowered.downloads.test.artifacts.server.lib.git; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.spongepowered.downloads.artifacts.server.lib.git.GitResolver; + +@MicronautTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestResourcesScope("testcontainers") +public class GitResolverTest { + + @Inject GitResolver resolver; + + @Test + void testResolveInvalidURL() { + final var either = resolver.validateRepository("not://a/valid/url"); + final var join = either.join(); + Assertions.assertTrue(join.isLeft()); + Assertions.assertFalse(join.isRight()); + } + + @Test + void testResolveValidURL() { + final var either = resolver.validateRepository("https://github.com/SpongePowered/SpongeAPI.git"); + final var join = either.join(); + Assertions.assertTrue(join.isRight()); + Assertions.assertFalse(join.isLeft()); + } + + @Test + void testResolveUnsupportedTransport() { + final var either = resolver.validateRepository("git://github.com/SpongePowered/SpongeAPI.git"); + final var join = either.join(); + Assertions.assertTrue(join.isLeft()); + Assertions.assertFalse(join.isRight()); + } +} diff --git a/artifacts/server/src/test/resources/application-test.conf b/artifacts/server/src/test/resources/application-test.conf index a4090ab5..5c9960fd 100644 --- a/artifacts/server/src/test/resources/application-test.conf +++ b/artifacts/server/src/test/resources/application-test.conf @@ -6,5 +6,41 @@ akka { "org.spongepowered.downloads.akka.AkkaSerializable" = jackson-json } } + management.cluster { + bootstrap { + contact-point-discovery { + discovery-method = config + service-name = "service1" + port-name = "http" + required-contact-point-nr = 0 + } + } + } + cluster { + roles = ["Master"] + sharding { + role = "Master" + } + + } + +} +akka.extensions = ["akka.management.cluster.bootstrap.ClusterBootstrap"] +akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.postgres} +akka.persistence.journal.plugin = "akka.persistence.r2dbc.journal" +akka.persistence.snapshot-store.plugin = "akka.persistence.r2dbc.snapshot" +akka.persistence.state.plugin = "akka.persistence.r2dbc.state" +akka.persistence.r2dbc.connection-factory { + driver = "tc" + url = "r2dbc:tc:postgresql:///postgres?TC_IMAGE_TAG=15-alpine" + database = "testdb" + user = "testuser" + password = "testpassword" + protocol = "postgresql" + # ssl { + # enabled = on + # mode = "VERIFY_CA" + # root-cert = "/path/db_root.crt" + # } } diff --git a/artifacts/server/src/test/resources/application-test.yaml b/artifacts/server/src/test/resources/application-test.yaml index 7ab844b7..83e2e471 100644 --- a/artifacts/server/src/test/resources/application-test.yaml +++ b/artifacts/server/src/test/resources/application-test.yaml @@ -5,12 +5,12 @@ test-resources: username: testuser password: testpassword db-name: testdb - + port: 7654 liquibase: enabled: true datasources: default: - change-log: 'classpath:db/liquibase-changelog.xml' # (4) + change-log: 'classpath:db/test-changelog.xml' endpoints: liquibase: enabled: true diff --git a/artifacts/server/src/test/resources/db/changelog/1001-test-insert-artifacts.xml b/artifacts/server/src/test/resources/db/changelog/1001-test-insert-artifacts.xml new file mode 100644 index 00000000..3390bbaa --- /dev/null +++ b/artifacts/server/src/test/resources/db/changelog/1001-test-insert-artifacts.xml @@ -0,0 +1,16 @@ + + + + + SET SEARCH_PATH TO artifact; + INSERT INTO artifacts (group_id, artifact_id, display_name, website, git_repository, issues) VALUES + ('com.example', 'example', 'Example', 'https://www.example.com', 'https://example.com/', + 'https://example.com'); + + + + diff --git a/artifacts/server/src/test/resources/db/test-changelog.xml b/artifacts/server/src/test/resources/db/test-changelog.xml new file mode 100644 index 00000000..46048a82 --- /dev/null +++ b/artifacts/server/src/test/resources/db/test-changelog.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/ArtifactReadside.java b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/ArtifactReadside.java index 50d8178d..738386cf 100644 --- a/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/ArtifactReadside.java +++ b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/ArtifactReadside.java @@ -24,19 +24,30 @@ */ package org.spongepowered.downloads.artifacts.worker.readside; +import akka.Done; import akka.persistence.query.typed.EventEnvelope; import akka.projection.r2dbc.javadsl.R2dbcHandler; +import akka.projection.r2dbc.javadsl.R2dbcSession; import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifacts.events.DetailsEvent; + +import java.util.concurrent.CompletionStage; @Singleton public class ArtifactReadside extends R2dbcHandler> { - @Inject - public ArtifactReadside(final ReadSide readSide) { - readSide.register(DetailsWriter.class); + public ArtifactReadside() { + } + + @Override + public CompletionStage process( + final R2dbcSession session, final EventEnvelope detailsEventEventEnvelope + ) throws Exception, Exception { + + return null; } static final class DetailsWriter extends ReadSideProcessor { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bbdb406c..5a49a30d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -micronaut = "4.1.3" +micronaut = "4.2.3" scala = "2.13" -akka = "2.9.5" +akka = "2.9.6" jackson = "2.17.2" maven_artifact = "3.9.9" akkaManagementVersion = "1.5.3" @@ -9,6 +9,8 @@ akkaProjection = "1.5.5" akkaR2DBC = "1.2.5" vavr = "0.10.4" jakartaValidation = "3.0.2" +jgit = "6.8.0.202311291450-r" +akkaDiagnostics = "2.1.0" [libraries] vavr = { module = "io.vavr:vavr", version.ref = "vavr"} @@ -18,8 +20,9 @@ akka-cluster-sharding = { module = "com.typesafe.akka:akka-cluster-sharding-type akka-cluster-typed = { module = "com.typesafe.akka:akka-cluster-typed_2.13" } akka-testkit = { module = "com.typesafe.akka:akka-actor-testkit-typed_2.13"} +akka-persistence-testkit = { module = "com.typesafe.akka:akka-persistence-testkit_2.13" } -akka-persistence = { module ="com.typesafe.akka:akka-persistence-typed_2.13"} +akka-persistence-core = { module ="com.typesafe.akka:akka-persistence-typed_2.13"} akka-projection = { module = "com.lightbend.akka:akka-projection-r2dbc_2.13", version.ref = "akkaProjection"} akka-r2dbc = { module = "com.lightbend.akka:akka-persistence-r2dbc_2.13", version.ref = "akkaR2DBC"} postgres-r2dbc = { module = "org.postgresql:r2dbc-postgresql"} @@ -28,6 +31,8 @@ akka-discovery = { module = "com.typesafe.akka:akka-discovery_2.13" } lightbend_management = { module = "com.lightbend.akka.management:akka-management_2.13", version.ref = "akkaManagementVersion"} lightbend_bootstrap = { module = "com.lightbend.akka.management:akka-management-cluster-bootstrap_2.13", version.ref = "akkaManagementVersion"} +akka-diagnostics = { module = "com.lightbend.akka:akka-diagnostics_2.13", version.ref = "akkaDiagnostics"} + jacksonBom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } jackson-core = { module = "com.fasterxml.jackson.core:jackson-core" } jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations" } @@ -36,11 +41,15 @@ akka-jackson = { module = "com.typesafe.akka:akka-serialization-jackson_2.13"} maven = { module = "org.apache.maven:maven-artifact", version.ref = "maven_artifact" } jakarta-validation = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakartaValidation"} +jgit-core = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } +jgit-ssh = {module = "org.eclipse.jgit:org.eclipse.jgit.ssh.jsch", version.ref = "jgit" } [bundles] serder = ["jackson-core", "jackson-annotations", "jackson-databind"] appSerder = ["jackson-databind", "jackson-annotations", "jackson-core", "akka-jackson"] actors = ["akka-actor", "akka-cluster-typed", "akka-cluster-sharding"] -actorsPersistence = ["akka-persistence", "akka-projection", "akka-r2dbc", "postgres-r2dbc"] +actorsPersistence = ["akka-persistence-core", "akka-projection", "akka-r2dbc", "postgres-r2dbc"] akkaManagement = ["akka-discovery", "lightbend_bootstrap", "lightbend_management"] +git = ["jgit-core", "jgit-ssh"] + diff --git a/settings.gradle.kts b/settings.gradle.kts index 50233302..5ff4e838 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,12 @@ dependencyResolutionManagement { maven("https://repo.spongepowered.org/repository/maven-public/") { name = "sponge" } + maven("https://repo.akka.io/maven") { + content { + includeGroup("com.typesafe.akka") + includeGroup("com.lightbend.akka") + } + } } }