From 474a2f4c3e9e0feb66a6d01ee18c5b8f9ad524b9 Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Sat, 12 Oct 2024 00:22:58 -0700 Subject: [PATCH] chore: strip out akka Akka is proving to be too burdensome with micronaut's setup. --- .java-version | 2 +- akka/build.gradle.kts | 17 - akka/gradlew | 240 -------- akka/gradlew.bat | 91 --- .../downloads/akka/AkkaExtension.java | 68 --- .../downloads/akka/AkkaSerializable.java | 7 - .../downloads/akka/ProductionAkkaSystem.java | 29 - akka/src/main/resources/refrerence.conf | 23 - akka/testkit/build.gradle.kts | 15 - .../test/akka/AkkaTestExtension.java | 57 -- .../testkit/src/main/resources/reference.conf | 3 - artifacts/api/build.gradle.kts | 22 +- artifacts/api/src/main/java/module-info.java | 10 + .../artifact/api/ComparableVersion.java | 518 ++++++++++++++++++ .../artifact/api/MavenCoordinates.java | 1 - .../artifact/api/mutation/Update.java | 85 +-- .../artifact/api/query/ArtifactDetails.java | 2 + .../artifact/api/query/GroupRegistration.java | 20 +- .../artifact/api/query/GroupsResponse.java | 3 +- .../registration/ArtifactRegistration.java | 17 +- .../api/test/UpdateValidationTest.java | 83 +++ artifacts/events/build.gradle.kts | 2 +- .../events/src/main/java/module-info.java | 8 + .../artifacts/events/ArtifactEvent.java | 9 +- .../artifacts/events/DetailsEvent.java | 12 +- .../artifacts/events/GroupUpdate.java | 12 +- artifacts/server/build.gradle.kts | 67 +-- .../server/src/main/java/module-info.java | 18 + .../artifacts/server/Application.java | 9 - .../cmd/details/ArtifactDetailsEntity.java | 206 ------- .../server/cmd/details/DetailsCommand.java | 21 +- .../cmd/details/state/DetailsState.java | 3 +- .../cmd/details/state/PopulatedState.java | 3 +- .../server/cmd/group/ArtifactRepository.java | 12 + .../server/cmd/group/GroupCommand.java | 39 +- .../server/cmd/group/GroupEntity.java | 203 ------- .../server/cmd/group/GroupRepository.java | 13 + .../server/cmd/group/GroupService.java | 110 ++++ .../server/cmd/group/domain/Artifact.java | 77 +++ .../server/cmd/group/domain/Group.java | 77 +++ .../server/cmd/group/state/EmptyState.java | 54 -- .../server/cmd/group/state/GroupState.java | 40 -- .../cmd/group/state/PopulatedState.java | 53 -- .../transport/ArtifactCommandController.java | 218 +++----- .../cmd/transport/GroupCommandController.java | 37 ++ .../artifacts/server/lib/git/GitResolver.java | 45 +- .../server/query/group/GroupQueryService.java | 16 + .../server/query/meta/domain/JpaArtifact.java | 4 + .../transport/ArtifactQueryController.java | 10 - .../query/transport/GroupQueryController.java | 31 ++ .../src/main/resources/application.conf | 38 -- .../src/main/resources/application.yaml | 25 +- .../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 | 58 +- .../main/resources/db/liquibase-changelog.xml | 1 - .../artifacts/server/ApplicationTest.java | 6 - .../cmd/details/ArtifactDetailsTest.java | 72 --- .../server/cmd/group/GroupServiceTest.java | 25 + .../cmd/transport/ArtifactControllerTest.java | 49 +- .../server/lib/git/GitResolverTest.java | 13 +- .../src/test/resources/application-test.conf | 46 -- .../src/test/resources/application-test.yaml | 2 +- .../changelog/1001-test-insert-artifacts.xml | 10 +- .../src/test/resources/db/test-changelog.xml | 1 - .../server/src/test/resources/logback.xml | 2 +- artifacts/worker/build.gradle.kts | 9 - .../worker/readside/ArtifactReadside.java | 149 +++-- build.gradle.kts | 34 +- events/build.gradle.kts | 9 + .../test-resources/test-resources-port.txt | 1 + events/outbox/build.gradle.kts | 54 ++ events/outbox/src/main/java/module-info.java | 16 + .../downloads/events/outbox/OutboxEvent.java | 23 + .../events/outbox/OutboxProducer.java | 19 + .../events/outbox/OutboxRepository.java | 43 ++ .../events/outbox/OutboxService.java | 27 + .../src/main/resources/application.yaml | 28 + .../db/changelog/01-create-outbox.xml | 25 + .../main/resources/db/liquibase-changelog.xml | 9 + .../downloads/outbox/test/DemoEvent.java | 18 + .../downloads/outbox/test/OutboxRepoTest.java | 27 + .../outbox/test/OutboxServiceTest.java | 34 ++ .../src/test/resources/application-test.yaml | 21 + events/src/main/java/module-info.java | 3 + .../downloads/events/EventMarker.java | 12 + .../downloads/events/KafkaEvent.java | 4 + gradle/libs.versions.toml | 89 ++- settings.gradle.kts | 14 +- 89 files changed, 1888 insertions(+), 1926 deletions(-) delete mode 100644 akka/build.gradle.kts delete mode 100755 akka/gradlew delete mode 100644 akka/gradlew.bat delete mode 100644 akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java delete mode 100644 akka/src/main/java/org/spongepowered/downloads/akka/AkkaSerializable.java delete mode 100644 akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java delete mode 100644 akka/src/main/resources/refrerence.conf delete mode 100644 akka/testkit/build.gradle.kts delete mode 100644 akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java delete mode 100644 akka/testkit/src/main/resources/reference.conf create mode 100644 artifacts/api/src/main/java/module-info.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ComparableVersion.java create mode 100644 artifacts/api/src/test/java/org/spongepowered/downloads/artifact/api/test/UpdateValidationTest.java create mode 100644 artifacts/events/src/main/java/module-info.java create mode 100644 artifacts/server/src/main/java/module-info.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/ArtifactRepository.java delete 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/GroupRepository.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupService.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Artifact.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Group.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/EmptyState.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/GroupState.java delete 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/GroupCommandController.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/GroupQueryService.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/GroupQueryController.java delete mode 100644 artifacts/server/src/main/resources/application.conf delete mode 100644 artifacts/server/src/main/resources/db/akka/akka_001_init.sql delete mode 100644 artifacts/server/src/main/resources/db/akka/akka_2_8_2.xml delete 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/group/GroupServiceTest.java delete mode 100644 artifacts/server/src/test/resources/application-test.conf create mode 100644 events/build.gradle.kts create mode 100644 events/outbox/.micronaut/test-resources/test-resources-port.txt create mode 100644 events/outbox/build.gradle.kts create mode 100644 events/outbox/src/main/java/module-info.java create mode 100644 events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxEvent.java create mode 100644 events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxProducer.java create mode 100644 events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxRepository.java create mode 100644 events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxService.java create mode 100644 events/outbox/src/main/resources/application.yaml create mode 100644 events/outbox/src/main/resources/db/changelog/01-create-outbox.xml create mode 100644 events/outbox/src/main/resources/db/liquibase-changelog.xml create mode 100644 events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/DemoEvent.java create mode 100644 events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxRepoTest.java create mode 100644 events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxServiceTest.java create mode 100644 events/outbox/src/test/resources/application-test.yaml create mode 100644 events/src/main/java/module-info.java create mode 100644 events/src/main/java/org/spongepowered/downloads/events/EventMarker.java create mode 100644 events/src/main/java/org/spongepowered/downloads/events/KafkaEvent.java diff --git a/.java-version b/.java-version index aabe6ec..5f39e91 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -21 +21.0 diff --git a/akka/build.gradle.kts b/akka/build.gradle.kts deleted file mode 100644 index 99e22dc..0000000 --- a/akka/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ - -version = "0.1" -group = "org.spongepowered.downloads" - -plugins { - id("com.github.johnrengelman.shadow") - id("io.micronaut.library") -} - -dependencies { - annotationProcessor("io.micronaut.serde:micronaut-serde-processor") - implementation("io.micronaut.serde:micronaut-serde-jackson") - api("io.micronaut:micronaut-inject") - api(platform(libs.akkaBom)) - api(libs.bundles.actors) - implementation(libs.bundles.akkaManagement) -} diff --git a/akka/gradlew b/akka/gradlew deleted file mode 100755 index a69d9cb..0000000 --- a/akka/gradlew +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/akka/gradlew.bat b/akka/gradlew.bat deleted file mode 100644 index f127cfd..0000000 --- a/akka/gradlew.bat +++ /dev/null @@ -1,91 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java deleted file mode 100644 index d871941..0000000 --- a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.spongepowered.downloads.akka; - -import akka.actor.typed.ActorSystem; -import akka.actor.typed.Behavior; -import akka.actor.typed.Scheduler; -import akka.actor.typed.SpawnProtocol; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -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.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", 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) { - return ClusterSharding.get(system); - } - -} diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaSerializable.java b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaSerializable.java deleted file mode 100644 index 793d772..0000000 --- a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaSerializable.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.spongepowered.downloads.akka; - -/** - * Marker interface for Akka serialization via Jackson - */ -public interface AkkaSerializable { -} diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java b/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java deleted file mode 100644 index cd971e5..0000000 --- a/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.spongepowered.downloads.akka; - -import akka.actor.typed.Behavior; -import akka.actor.typed.SpawnProtocol; -import akka.actor.typed.javadsl.Behaviors; -import akka.management.cluster.bootstrap.ClusterBootstrap; -import akka.management.javadsl.AkkaManagement; -import io.micronaut.context.annotation.Bean; -import io.micronaut.context.annotation.Factory; - -@Factory -public class ProductionAkkaSystem { - - /** - * The {@link Behavior} for the production guardian. - * - * @return The behavior - */ - @Bean - public Behavior productionGuardian() { - return Behaviors.setup(ctx -> { - final var system = ctx.getSystem(); - ClusterBootstrap.get(system).start(); - AkkaManagement.get(system).start(); - return SpawnProtocol.create(); - }); - } - -} diff --git a/akka/src/main/resources/refrerence.conf b/akka/src/main/resources/refrerence.conf deleted file mode 100644 index 7aa8e90..0000000 --- a/akka/src/main/resources/refrerence.conf +++ /dev/null @@ -1,23 +0,0 @@ - -akka { - actor { - provider = "cluster" - serialization-bindings { - "org.spongepowered.downloads.akka.AkkaSerializable" = jackson-json - } - } - remote.artery { - canonical { - hostname = "127.0.0.1" - port = 2551 - } - } - - cluster { - seed-nodes = [ - "akka://ClusterSystem@127.0.0.1:2551", - "akka://ClusterSystem@127.0.0.1:2552"] - - downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" - } -} diff --git a/akka/testkit/build.gradle.kts b/akka/testkit/build.gradle.kts deleted file mode 100644 index b5d8759..0000000 --- a/akka/testkit/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ - - - -plugins { - id("com.github.johnrengelman.shadow") - id("io.micronaut.library") -} -dependencies { - annotationProcessor("io.micronaut.serde:micronaut-serde-processor") - implementation("io.micronaut.serde:micronaut-serde-jackson") - 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 deleted file mode 100644 index 643f700..0000000 --- a/akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.spongepowered.downloads.test.akka; - -import akka.actor.testkit.typed.javadsl.ActorTestKit; -import akka.actor.testkit.typed.javadsl.BehaviorTestKit; -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() { - return SpawnProtocol.create(); - } - - @Replaces - @Bean - public Config testConfig() { - return PersistenceTestKitPlugin.getInstance().config() - .withFallback(BehaviorTestKit.applicationTestConfig()) - .withFallback(ConfigFactory.defaultApplication()) - .resolve(); - } - - @Singleton - @Bean(preDestroy = "shutdownTestKit") - 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) - @Singleton - public ActorSystem system(@NonNull ActorTestKit kit) { - return kit.system(); - } - -} diff --git a/akka/testkit/src/main/resources/reference.conf b/akka/testkit/src/main/resources/reference.conf deleted file mode 100644 index 02455b3..0000000 --- a/akka/testkit/src/main/resources/reference.conf +++ /dev/null @@ -1,3 +0,0 @@ -systemofadownload { - clustering = false -} diff --git a/artifacts/api/build.gradle.kts b/artifacts/api/build.gradle.kts index 2391129..c8d4ae3 100644 --- a/artifacts/api/build.gradle.kts +++ b/artifacts/api/build.gradle.kts @@ -3,13 +3,29 @@ version = "0.1" group = "org.spongepowered.downloads" plugins { - `java-library` + id("io.micronaut.library") } +micronaut { + testRuntime("junit5") +} dependencies { api(platform(libs.jacksonBom)) api(libs.bundles.serder) - api(libs.maven) - api(libs.vavr) + + // Annotation processor of validation kinds + annotationProcessor(libs.bundles.validation.processors) + + // HTTP type validation + api(libs.jakarta.validation) + implementation(libs.micronaut.validation) + + + testAnnotationProcessor("io.micronaut:micronaut-inject-java") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("io.micronaut.test:micronaut-test-junit5") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.jupiter:junit-jupiter-params") } diff --git a/artifacts/api/src/main/java/module-info.java b/artifacts/api/src/main/java/module-info.java new file mode 100644 index 0000000..46fbe4e --- /dev/null +++ b/artifacts/api/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module org.spongepowered.downloads.artifacts.api { + exports org.spongepowered.downloads.artifact.api; + exports org.spongepowered.downloads.artifact.api.query; + exports org.spongepowered.downloads.artifact.api.mutation; + exports org.spongepowered.downloads.artifact.api.registration; + + requires com.fasterxml.jackson.databind; + requires jakarta.validation; + requires io.micronaut.core; +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ComparableVersion.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ComparableVersion.java new file mode 100644 index 0000000..a32dc22 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ComparableVersion.java @@ -0,0 +1,518 @@ +package org.spongepowered.downloads.artifact.api; + + +import java.math.BigInteger; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + *

+ * Generic implementation of version comparison. + *

+ * + * Features: + *
    + *
  • mixing of '-' (hyphen) and '.' (dot) separators,
  • + *
  • transition between characters and digits also constitutes a separator: + * 1.0alpha1 => [1, 0, alpha, 1]
  • + *
  • unlimited number of version components,
  • + *
  • version components in the text can be digits or strings,
  • + *
  • strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. + * Well-known qualifiers (case insensitive) are:
      + *
    • alpha or a
    • + *
    • beta or b
    • + *
    • milestone or m
    • + *
    • rc or cr
    • + *
    • snapshot
    • + *
    • (the empty string) or ga or final
    • + *
    • sp
    • + *
    + * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), + *
  • + *
  • a hyphen usually precedes a qualifier, and is always less important than digits/number, for example + * {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC1} over {@code 1.0.0.RC1}, and more + * generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but prefer {@code 1.0.0-X1} + * over {@code 1.0.0.X1}.
  • + *
+ * + * @see "Versioning" on Maven Wiki + * @author Kenney Westerhof + * @author Hervé Boutemy + */ +public class ComparableVersion implements Comparable { + private static final int MAX_INTITEM_LENGTH = 9; + + private static final int MAX_LONGITEM_LENGTH = 18; + + private String value; + + private String canonical; + + private ComparableVersion.ListItem items; + + private interface Item { + int INT_ITEM = 3; + int LONG_ITEM = 4; + int BIGINTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(ComparableVersion.Item item); + + int getType(); + + boolean isNull(); + } + + /** + * Represents a numeric item in the version item list that can be represented with an int. + */ + private record IntItem(int value) implements ComparableVersion.Item { + + public static final ComparableVersion.IntItem ZERO = new ComparableVersion.IntItem(); + + private IntItem() { + this(0); + } + + IntItem(String str) { + this(Integer.parseInt(str)); + } + + @Override + public int getType() { + return INT_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(ComparableVersion.Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + return switch (item.getType()) { + case INT_ITEM -> { + int itemValue = ((IntItem) item).value; + yield Integer.compare(value, itemValue); + } + case LONG_ITEM, BIGINTEGER_ITEM -> -1; + case STRING_ITEM -> 1; // 1.1 > 1-sp + + case LIST_ITEM -> 1; // 1.1 > 1-1 + + default -> throw new IllegalStateException("invalid item: " + item.getClass()); + }; + } + + @Override + public String toString() { + return Integer.toString(value); + } + } + + /** + * Represents a numeric item in the version item list that can be represented with a long. + */ + private static record LongItem(long value) implements ComparableVersion.Item { + + LongItem(String str) { + this(Long.parseLong(str)); + } + + @Override + public int getType() { + return LONG_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(ComparableVersion.Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + return switch (item.getType()) { + case INT_ITEM -> 1; + case LONG_ITEM -> { + long itemValue = ((LongItem) item).value; + yield Long.compare(value, itemValue); + } + case BIGINTEGER_ITEM -> -1; + case STRING_ITEM -> 1; // 1.1 > 1-sp + + case LIST_ITEM -> 1; // 1.1 > 1-1 + + default -> throw new IllegalStateException("invalid item: " + item.getClass()); + }; + } + + @Override + public String toString() { + return Long.toString(value); + } + } + + /** + * Represents a numeric item in the version item list. + */ + private static record BigIntegerItem(BigInteger value) implements ComparableVersion.Item { + + BigIntegerItem(String str) { + this(new BigInteger(str)); + } + + @Override + public int getType() { + return BIGINTEGER_ITEM; + } + + @Override + public boolean isNull() { + return BigInteger.ZERO.equals(value); + } + + @Override + public int compareTo(ComparableVersion.Item item) { + if (item == null) { + return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + return switch (item.getType()) { + case INT_ITEM, LONG_ITEM -> 1; + case BIGINTEGER_ITEM -> value.compareTo(((BigIntegerItem) item).value); + case STRING_ITEM -> 1; // 1.1 > 1-sp + + case LIST_ITEM -> 1; // 1.1 > 1-1 + + default -> throw new IllegalStateException("invalid item: " + item.getClass()); + }; + } + + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private record StringItem(String value) implements ComparableVersion.Item { + private static final List QUALIFIERS = + Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp"); + + private static final Properties ALIASES = new Properties(); + + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("release", ""); + ALIASES.put("cr", "rc"); + } + + /** + * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes + * the version older than one without a qualifier, or more recent. + */ + private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf("")); + + StringItem(String value, boolean followedByDigit) { + this(deriveValueFollowedByDigit(value, followedByDigit)); + } + + private static String deriveValueFollowedByDigit(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + default: + } + } + return ALIASES.getProperty(value, value); + } + + @Override + public int getType() { + return STRING_ITEM; + } + + @Override + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + public static String comparableQualifier(String qualifier) { + int i = QUALIFIERS.indexOf(qualifier); + + return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + } + + @Override + public int compareTo(ComparableVersion.Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + return switch (item.getType()) { + case INT_ITEM, LONG_ITEM, BIGINTEGER_ITEM -> -1; // 1.any < 1.1 ? + + case STRING_ITEM -> + comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + case LIST_ITEM -> -1; // 1.any < 1-1 + + default -> throw new IllegalStateException("invalid item: " + item.getClass()); + }; + } + + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + private static class ListItem extends ArrayList + implements ComparableVersion.Item { + @Override + public int getType() { + return LIST_ITEM; + } + + @Override + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (int i = size() - 1; i >= 0; i--) { + ComparableVersion.Item lastItem = get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); + } else if (!(lastItem instanceof ComparableVersion.ListItem)) { + break; + } + } + } + + @Override + public int compareTo(ComparableVersion.Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + // Compare the entire list of items with null - not just the first one, MNG-6964 + for (ComparableVersion.Item i : this) { + int result = i.compareTo(null); + if (result != 0) { + return result; + } + } + return 0; + } + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ComparableVersion.ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + ComparableVersion.Item l = left.hasNext() ? left.next() : null; + ComparableVersion.Item r = right.hasNext() ? right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (ComparableVersion.Item item : this) { + if (!buffer.isEmpty()) { + buffer.append((item instanceof ComparableVersion.ListItem) ? '-' : '.'); + } + buffer.append(item); + } + return buffer.toString(); + } + } + + public ComparableVersion(String version) { + parseVersion(version); + } + + @SuppressWarnings("checkstyle:innerassignment") + public final void parseVersion(String version) { + this.value = version; + + items = new ComparableVersion.ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ComparableVersion.ListItem list = items; + + Deque stack = new ArrayDeque<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(ComparableVersion.IntItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(ComparableVersion.IntItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + list.add(list = new ComparableVersion.ListItem()); + stack.push(list); + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + // 1.0.0.X1 < 1.0.0-X2 + // treat .X as -X for any string qualifier X + if (!list.isEmpty()) { + list.add(list = new ComparableVersion.ListItem()); + stack.push(list); + } + + list.add(new ComparableVersion.StringItem(version.substring(startIndex, i), true)); + startIndex = i; + + list.add(list = new ComparableVersion.ListItem()); + stack.push(list); + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ComparableVersion.ListItem()); + stack.push(list); + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + // 1.0.0.X1 < 1.0.0-X2 + // treat .X as -X for any string qualifier X + if (!isDigit && !list.isEmpty()) { + list.add(list = new ComparableVersion.ListItem()); + stack.push(list); + } + + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ComparableVersion.ListItem) stack.pop(); + list.normalize(); + } + } + + private static ComparableVersion.Item parseItem(boolean isDigit, String buf) { + if (isDigit) { + buf = stripLeadingZeroes(buf); + if (buf.length() <= MAX_INTITEM_LENGTH) { + // lower than 2^31 + return new ComparableVersion.IntItem(buf); + } else if (buf.length() <= MAX_LONGITEM_LENGTH) { + // lower than 2^63 + return new ComparableVersion.LongItem(buf); + } + return new ComparableVersion.BigIntegerItem(buf); + } + return new ComparableVersion.StringItem(buf, false); + } + + private static String stripLeadingZeroes(String buf) { + if (buf == null || buf.isEmpty()) { + return "0"; + } + for (int i = 0; i < buf.length(); ++i) { + char c = buf.charAt(i); + if (c != '0') { + return buf.substring(i); + } + } + return buf; + } + + @Override + public int compareTo(ComparableVersion o) { + return items.compareTo(o.items); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items); + } + + @Override + public int hashCode() { + return items.hashCode(); + } +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java index c1acb52..ef70fd0 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java @@ -28,7 +28,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.apache.maven.artifact.versioning.ComparableVersion; import java.util.Objects; import java.util.StringJoiner; 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 index 69bb381..96926c7 100644 --- 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 @@ -4,72 +4,77 @@ 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 io.micronaut.core.annotation.Introspected; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; -import java.net.URI; -import java.net.URL; +import java.util.List; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonDeserialize -public sealed interface Update { - - Either validate(); +public sealed interface Update { + // This is a relatively simple regex that should cover most validation cases + // It is not perfect and may not cover all edge cases, but realistically + // we're only checking for basic URL format to link to an issues page or + // something similar. + String URL_REGEX = "^(https?):\\/\\/(www\\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(:[0-9]{1,5})?(/[a-zA-Z0-9-._~:/?#/@!$&'()*+,;=%]*)?$"; + // And this is yet a different git url validation regex, only to verify + // since we need to be able to query git repositories. + String GIT_URL_PATTERN = "^(https://|git://|git@)([a-zA-Z0-9.-]+)[:/]([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)(\\.git)$"; @JsonTypeName("website") + @Introspected record Website( - @JsonProperty(required = true) String website - ) implements Update { + @Pattern(regexp = URL_REGEX, + message = "Invalid URL format") + @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") + @Introspected record DisplayName( - @JsonProperty(required = true) String display - ) implements Update { - - @Override - public Either validate() { - return Either.right(this.display); - } + @NotBlank + @Size(min = 1, max = 255) + @JsonProperty(required = true) + String display + ) implements Update { } @JsonTypeName("issues") + @Introspected record Issues( + @Pattern(regexp = URL_REGEX, + message = "Invalid URL format") @JsonProperty(required = true) String issues - ) implements Update { + ) 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") + @Introspected record GitRepository( + @Pattern(regexp = GIT_URL_PATTERN, + message = "Invalid URL format") @JsonProperty(required = true) String gitRepo - ) implements Update { + ) 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))); - } + @JsonTypeName("git-repos") + @Introspected + record GitRepositories( + @Pattern(regexp = GIT_URL_PATTERN, + message = "Invalid URL format") + @JsonProperty(required = true) + List gitRepos + ) implements Update { } } 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 e90806e..84244cd 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 @@ -43,6 +43,8 @@ record Ok( } + record ValidRepo(String url) implements Response {} + 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/GroupRegistration.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java index b127e32..417626a 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java @@ -27,15 +27,29 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.mutation.Update; public final class GroupRegistration { + @Introspected @JsonDeserialize public record RegisterGroupRequest( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String website + @NotBlank + @Size(min = 1, max = 255) + @JsonProperty(required = true) + String name, + @NotBlank + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9._-]+$", message = "Invalid group coordinates") + @JsonProperty(required = true) + String groupCoordinates, + @Pattern(regexp = Update.URL_REGEX, message = "Invalid URL format") + @JsonProperty(required = true) + String website ) { @JsonCreator diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java index 7841665..d8d89eb 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java @@ -29,9 +29,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; import org.spongepowered.downloads.artifact.api.Group; +import java.util.List; + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = GroupsResponse.Available.class, name = "Groups") diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java index 13d49b8..c3e0b3f 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/registration/ArtifactRegistration.java @@ -25,19 +25,24 @@ 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; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; public final class ArtifactRegistration { + @Introspected @JsonSerialize public record RegisterArtifact( - @JsonProperty(required = true) String artifactId, - @JsonProperty(required = true) String displayName + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9._-]+$", message = "Invalid artifact ID") + @JsonProperty(required = true) + String artifactId, + @NotBlank + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9._-]+$", message = "Invalid display name") + @JsonProperty(required = true) + String displayName ) { @JsonCreator diff --git a/artifacts/api/src/test/java/org/spongepowered/downloads/artifact/api/test/UpdateValidationTest.java b/artifacts/api/src/test/java/org/spongepowered/downloads/artifact/api/test/UpdateValidationTest.java new file mode 100644 index 0000000..a12584f --- /dev/null +++ b/artifacts/api/src/test/java/org/spongepowered/downloads/artifact/api/test/UpdateValidationTest.java @@ -0,0 +1,83 @@ +package org.spongepowered.downloads.artifact.api.test; + + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import jakarta.validation.Validator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.spongepowered.downloads.artifact.api.mutation.Update; + +@MicronautTest(startApplication = false) +public class UpdateValidationTest { + + @Inject + Validator validator; + + @ParameterizedTest + @ValueSource(strings = { + "htp://www.example.com", // Incorrect protocol + "http://example..com", // Double dot in domain + "http://.example.com", // Leading dot in domain + "http://example_com", // Underscore in domain + "http://example.com,com", // Comma in domain + "http://example..com/page", // Double dot in URL path + "://example.com", // Missing protocol + "http://example .com", // Space in domain + "http:///example.com", // Triple slash after protocol + "http://example.com:-80", // Negative port number + "http://example..com", // Repeated dot in domain name + "http://-example.com", // Leading hyphen in domain name + "http://example.com/invalid|character", // Invalid character '|' + "ftp://example.com", // Unsupported protocol (if only http and https are allowed) + "http://256.256.256.256", // Invalid IP address + "http://.com", // Only TLD without domain name + "http://example..com../page", // Multiple double dots in domain and path + "http://example!.com", // Invalid character '!' + "http://example@com", // '@' instead of '.' + "http://www.exam_ple.com" // Underscore in the subdomain + }) + void testInvalidUrls(String url) { + assertFalse(validator.validate(new Update.Website(url)).isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { + "https://github.com/user/repo.git", // HTTPS with .git + "https://github.com/user/repo", // HTTPS without .git + "git://github.com/user/repo.git", // Git protocol with .git + "git://github.com/user/repo", // Git protocol without .git + "git@github.com:user/repo.git", // SSH with .git + "git@github.com:user/repo", // SSH without .git + "https://gitlab.com/organization/project.git", // HTTPS with .git + "https://bitbucket.org/team/repo.git" // HTTPS with .git on Bitbucket + }) + void testValidGitUrls(String url) { + assertTrue(validator.validate(new Update.GitRepository(url)).isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { + "http://github.com/user/repo.git", // Invalid protocol (http instead of https) + "https//github.com/user/repo.git", // Missing colon in https protocol + "git:/github.com/user/repo.git", // Missing slash after git: + "https://github.com/user/repo.tar.gz", // Incorrect file extension + "https://github.com/user/repo/", // Trailing slash at the end + "ftp://github.com/user/repo.git", // Unsupported protocol (ftp) + "git@github.com:user", // Missing repository name + "github.com:user/repo.git", // Missing protocol + "https://github.com/user/repo.git/extra", // Extra path segment after .git + "git@github.com:user/repo?.git", // Special character '?' in URL + "git@github.com:user/repo@", // Ending with '@' + "git://github.com/user/repo/repo.git", // Extra path segment before .git + "https://github.com/user/repo.git repo", // Space in URL + "git@github.com:user//repo.git" // Double slashes in the URL + }) + void testInvalidGitUrls(String url) { + assertFalse(validator.validate(new Update.GitRepository(url)).isEmpty()); + } + +} diff --git a/artifacts/events/build.gradle.kts b/artifacts/events/build.gradle.kts index 56e6180..bcf212b 100644 --- a/artifacts/events/build.gradle.kts +++ b/artifacts/events/build.gradle.kts @@ -7,7 +7,7 @@ plugins { dependencies { api(project(":artifacts:api")) - api(project(":akka")) + api(project(":events")) } diff --git a/artifacts/events/src/main/java/module-info.java b/artifacts/events/src/main/java/module-info.java new file mode 100644 index 0000000..d25b9d0 --- /dev/null +++ b/artifacts/events/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module org.spongepowered.downloads.artifacts.events { + + exports org.spongepowered.downloads.artifacts.events; + + requires org.spongepowered.downloads.artifacts.api; + requires com.fasterxml.jackson.databind; + requires org.spongepowered.downloads.events; +} 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 cd29221..8692a19 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 @@ -4,11 +4,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.events.EventMarker; +import org.spongepowered.downloads.events.KafkaEvent; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -public sealed interface ArtifactEvent extends AkkaSerializable { +public sealed interface ArtifactEvent extends EventMarker { ArtifactCoordinates coordinates(); @@ -16,6 +17,10 @@ default String partitionKey() { return this.coordinates().asMavenString(); } + default String topic() { + return "ArtifactsArtifactUpserted"; + } + @JsonTypeName("registered") @JsonDeserialize record ArtifactRegistered( 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 e84d556..369e53c 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 @@ -28,14 +28,22 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.events.EventMarker; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -public interface DetailsEvent extends AkkaSerializable { +public interface DetailsEvent extends EventMarker { ArtifactCoordinates coordinates(); + default String partitionKey() { + return this.coordinates().asMavenString(); + } + + default String topic() { + return "ArtifactsArtifactDetailsUpserted"; + } + @JsonDeserialize @JsonTypeName("registered") record ArtifactRegistered( diff --git a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/GroupUpdate.java b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/GroupUpdate.java index 9f4253b..f2142d3 100644 --- a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/GroupUpdate.java +++ b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/GroupUpdate.java @@ -28,11 +28,19 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.events.EventMarker; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -public sealed interface GroupUpdate extends AkkaSerializable { +public sealed interface GroupUpdate extends EventMarker { + + default String partitionKey() { + return this.groupId(); + } + + default String topic() { + return "ArtifactsGroupUpserted"; + } String groupId(); diff --git a/artifacts/server/build.gradle.kts b/artifacts/server/build.gradle.kts index d62a5fb..0818f9e 100644 --- a/artifacts/server/build.gradle.kts +++ b/artifacts/server/build.gradle.kts @@ -43,61 +43,38 @@ graalvmNative.toolchainDetection.set(false) dependencies { implementation(project(":artifacts:api")) implementation(project(":artifacts:events")) - implementation(project(":akka")) - implementation(libs.vavr) - 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") + implementation(project(":events:outbox")) + // databases implementation(libs.bundles.appSerder) - implementation(libs.bundles.akkaManagement) - implementation(libs.bundles.actorsPersistence) + runtimeOnly(libs.bundles.postgres.runtime) + implementation(libs.bundles.postgres.r2dbc) + annotationProcessor(libs.bundles.postgres.annotations) - // databases - implementation("io.micronaut.data:micronaut-data-r2dbc") - 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") + // Serder + annotationProcessor(libs.bundles.serder.processor) + implementation(libs.bundles.serder) - // Micronaut - implementation("io.micronaut:micronaut-http-client-jdk") - implementation("io.micronaut:micronaut-management") + // Validation + annotationProcessor(libs.bundles.validation.processors) -// implementation(libs.lightbend.management) -// implementation(libs.lightbend.bootstrap) -// implementation(libs.akka.discovery) - implementation(libs.akka.diagnostics) + // Add micronaut test resources + testImplementation(libs.bundles.micronaut.testresources) + testImplementation(libs.bundles.junit) + testImplementation(libs.bundles.postgres.test) + testRuntimeOnly(libs.bundles.junit.runtime) + testResourcesService(libs.postgres.driver) + implementation(libs.bundles.git) - 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") + runtimeOnly(libs.bundles.micronaut.runtime) - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - testResourcesService("org.postgresql:postgresql") - compileOnly("org.graalvm.nativeimage:svm") + // Micronaut + implementation(libs.bundles.micronaut.http) - testImplementation("org.junit.jupiter:junit-jupiter-engine") + // GraalVM + compileOnly("org.graalvm.nativeimage:svm") -//// compileOnly("org.graalvm.nativeimage:svm") -// -// implementation("io.micronaut:micronaut-validation") } diff --git a/artifacts/server/src/main/java/module-info.java b/artifacts/server/src/main/java/module-info.java new file mode 100644 index 0000000..4b9899b --- /dev/null +++ b/artifacts/server/src/main/java/module-info.java @@ -0,0 +1,18 @@ +module org.spongepowered.downloads.artifacts.server { + requires io.micronaut.core; + requires io.micronaut.data.micronaut_data_r2dbc; + requires io.micronaut.http; + requires io.micronaut.inject; + requires io.micronaut.serde.micronaut_serde_api; + requires jakarta.persistence; + requires jakarta.validation; + requires org.eclipse.jgit; + requires org.spongepowered.downloads.artifacts.api; + requires org.spongepowered.downloads.artifacts.events; + requires org.spongepowered.downloads.events.outbox; + requires io.micronaut.data.micronaut_data_tx; + requires jakarta.inject; + requires com.fasterxml.jackson.databind; + requires io.micronaut.data.micronaut_data_model; + requires reactor.core; +} 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 5ef6c4e..f01dc0c 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,5 @@ package org.spongepowered.downloads.artifacts.server; -import akka.actor.typed.ActorSystem; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; import io.micronaut.context.annotation.Factory; import io.micronaut.runtime.Micronaut; import io.micronaut.runtime.event.annotation.EventListener; @@ -13,16 +11,9 @@ @Factory public class Application { - private final ActorSystem system; - private final ClusterSharding sharding; - @Inject public Application( - final ActorSystem system, - final ClusterSharding sharding ) { - this.system = system; - this.sharding = sharding; } public static void main(String[] args) { diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java deleted file mode 100644 index 6e319c8..0000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/ArtifactDetailsEntity.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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.details; - -import akka.NotUsed; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -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.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; -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.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 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; - private final ActorContext ctx; - - private ArtifactDetailsEntity( - ActorContext ctx, - final EntityContext context, - String entityId, PersistenceId persistenceId - ) { - super(persistenceId); - this.artifactId = entityId; - this.ctx = ctx; - } - - public static Behavior create( - final EntityContext context, - String entityId, PersistenceId persistenceId - ) { - return Behaviors.setup(ctx -> new ArtifactDetailsEntity(ctx, context, entityId, persistenceId)); - } - - @Override - public DetailsState emptyState() { - return new EmptyState(); - } - - @Override - public EventHandler eventHandler() { - final var builder = this.newEventHandlerBuilder(); - - builder.forAnyState() - .onEvent( - DetailsEvent.ArtifactRegistered.class, - (state, event) -> new PopulatedState( - event.coordinates(), state.displayName(), state.website(), state.gitRepository(), state.issues()) - ); - builder.forStateType(PopulatedState.class) - .onEvent(DetailsEvent.ArtifactDetailsUpdated.class, PopulatedState::withDisplayName) - .onEvent(DetailsEvent.ArtifactGitRepositoryUpdated.class, PopulatedState::withGitRepo) - .onEvent(DetailsEvent.ArtifactIssuesUpdated.class, PopulatedState::withIssues) - .onEvent(DetailsEvent.ArtifactWebsiteUpdated.class, PopulatedState::withWebsite); - - return builder.build(); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder(); - - builder.forStateType(EmptyState.class) - .onCommand( - DetailsCommand.RegisterArtifact.class, - (cmd) -> this.Effect() - .persist(List.of( - new DetailsEvent.ArtifactRegistered(cmd.coordinates()), - new DetailsEvent.ArtifactDetailsUpdated(cmd.coordinates(), cmd.displayName()) - )) - .thenReply(cmd.replyTo(), (state) -> NotUsed.notUsed()) - ) - .onCommand( - DetailsCommand.UpdateIssues.class, - cmd -> this.Effect().reply(cmd.replyTo(), NOT_FOUND) - ) - .onCommand( - DetailsCommand.UpdateWebsite.class, - cmd -> this.Effect().reply(cmd.replyTo(), NOT_FOUND) - ) - .onCommand( - DetailsCommand.UpdateDisplayName.class, - cmd -> this.Effect().reply(cmd.replyTo(), NOT_FOUND) - ) - .onCommand( - DetailsCommand.UpdateGitRepository.class, - cmd -> this.Effect().reply(cmd.replyTo(), NOT_FOUND) - ); - - builder.forStateType(PopulatedState.class) - .onCommand( - DetailsCommand.RegisterArtifact.class, - (s, cmd) -> this.Effect().reply(cmd.replyTo(), NotUsed.notUsed()) - ) - .onCommand( - DetailsCommand.UpdateIssues.class, - (s, cmd) -> this.Effect() - .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.validUrl().toString())) - .thenReply( - cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) - ) - ) - .onCommand( - DetailsCommand.UpdateWebsite.class, - (s, cmd) -> this.Effect() - .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.website().toString())) - .thenReply( - cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) - ) - ) - .onCommand( - DetailsCommand.UpdateDisplayName.class, - (s, cmd) -> this.Effect() - .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.displayName())) - .thenReply( - cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) - ) - ) - .onCommand( - DetailsCommand.UpdateGitRepository.class, - (s, cmd) -> this.Effect() - .persist(new DetailsEvent.ArtifactGitRepositoryUpdated(s.coordinates(), cmd.gitRemote())) - .thenReply( - cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response.Ok( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) - ) - ); - - return builder.build(); - } - -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java index 92916d2..980728e 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/DetailsCommand.java @@ -24,16 +24,11 @@ */ package org.spongepowered.downloads.artifacts.server.cmd.details; -import akka.NotUsed; -import akka.actor.typed.ActorRef; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.control.Either; -import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; import java.net.URL; @@ -52,11 +47,11 @@ @JsonSubTypes.Type(value = DetailsCommand.UpdateDisplayName.class, name = "display-name") }) -public sealed interface DetailsCommand extends AkkaSerializable { +public sealed interface DetailsCommand { @JsonDeserialize record RegisterArtifact(ArtifactCoordinates coordinates, - String displayName, ActorRef replyTo) + String displayName) implements DetailsCommand { @JsonCreator @@ -67,8 +62,7 @@ record RegisterArtifact(ArtifactCoordinates coordinates, @JsonDeserialize record UpdateWebsite( ArtifactCoordinates coordinates, - URL website, - ActorRef> replyTo + URL website ) implements DetailsCommand { @JsonCreator @@ -79,8 +73,7 @@ record UpdateWebsite( @JsonDeserialize record UpdateDisplayName( ArtifactCoordinates coordinates, - String displayName, - ActorRef> replyTo + String displayName ) implements DetailsCommand { @JsonCreator @@ -91,8 +84,7 @@ record UpdateDisplayName( @JsonDeserialize record UpdateGitRepository( ArtifactCoordinates coordinates, - String gitRemote, - ActorRef> replyTo + String gitRemote ) implements DetailsCommand { @JsonCreator @@ -103,8 +95,7 @@ record UpdateGitRepository( @JsonDeserialize record UpdateIssues( ArtifactCoordinates coords, - URL validUrl, - ActorRef> replyTo + URL validUrl ) implements DetailsCommand { @JsonCreator diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java index 05a47a9..5d399a5 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/DetailsState.java @@ -24,10 +24,9 @@ */ package org.spongepowered.downloads.artifacts.server.cmd.details.state; -import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -public interface DetailsState extends AkkaSerializable { +public interface DetailsState { ArtifactCoordinates coordinates(); String displayName(); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java index 9847f3a..e7d10de 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/details/state/PopulatedState.java @@ -26,14 +26,13 @@ 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, AkkaSerializable { + String issues) implements DetailsState { @JsonCreator public PopulatedState { diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/ArtifactRepository.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/ArtifactRepository.java new file mode 100644 index 0000000..b49dd7d --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/ArtifactRepository.java @@ -0,0 +1,12 @@ +package org.spongepowered.downloads.artifacts.server.cmd.group; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.repository.CrudRepository; +import org.spongepowered.downloads.artifacts.server.cmd.group.domain.Artifact; +import org.spongepowered.downloads.artifacts.server.cmd.group.domain.Group; + +@R2dbcRepository(dialect = Dialect.POSTGRES) +interface ArtifactRepository extends CrudRepository { + boolean existsByArtifactIdAndGroup(String artifactId, Group group); +} 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 index 4996396..1800e2d 100644 --- 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 @@ -24,47 +24,26 @@ */ 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; +public sealed interface GroupCommand { - -@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 + String name ) implements GroupCommand { - } record RegisterGroup( String mavenCoordinates, String name, - String website, - ActorRef replyTo + String website ) implements GroupCommand { + } + record UpdateGroup( + String groupId, + String name, + String website + ) 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 deleted file mode 100644 index 8bfdb6c..0000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupEntity.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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/GroupRepository.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupRepository.java new file mode 100644 index 0000000..123d136 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupRepository.java @@ -0,0 +1,13 @@ +package org.spongepowered.downloads.artifacts.server.cmd.group; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.repository.CrudRepository; +import org.spongepowered.downloads.artifacts.server.cmd.group.domain.Group; + +import java.util.Optional; + +@R2dbcRepository(dialect = Dialect.POSTGRES) +public interface GroupRepository extends CrudRepository { + Optional findByGroupId(String groupId); +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupService.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupService.java new file mode 100644 index 0000000..9151e55 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/GroupService.java @@ -0,0 +1,110 @@ +/* + * 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 io.micronaut.http.HttpResponse; +import io.micronaut.transaction.annotation.Transactional; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.validation.Valid; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.mutation.Update; +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.ArtifactEvent; +import org.spongepowered.downloads.artifacts.events.GroupUpdate; +import org.spongepowered.downloads.artifacts.server.cmd.group.domain.Artifact; +import org.spongepowered.downloads.artifacts.server.cmd.group.domain.Group; +import org.spongepowered.downloads.events.outbox.OutboxEvent; +import org.spongepowered.downloads.events.outbox.OutboxRepository; +import reactor.core.publisher.Mono; + +import java.util.Optional; + +@Singleton +public class GroupService { + + @Inject + private GroupRepository groupRepository; + + @Inject + private ArtifactRepository artifactRepository; + + @Inject + private OutboxRepository outboxRepository; + + @Transactional + public HttpResponse registerGroup(GroupCommand.RegisterGroup rg) { + Optional existingGroup = groupRepository.findByGroupId(rg.mavenCoordinates()); + if (existingGroup.isPresent()) { + return HttpResponse.badRequest(new GroupRegistration.Response.GroupAlreadyRegistered(rg.mavenCoordinates())); + } + + Group group = new Group(); + group.setGroupId(rg.mavenCoordinates()); + group.setName(rg.name()); + group.setWebsite(rg.website()); + groupRepository.save(group); + + OutboxEvent event = new OutboxEvent("GroupCreated", group.groupId(), new GroupUpdate.GroupRegistered(rg.mavenCoordinates(), rg.name(), rg.website())); + Mono.from(outboxRepository.save(event)).then().block(); + final var groupDTO = new org.spongepowered.downloads.artifact.api.Group(group.groupId(), group.name(), group.website()); + + return HttpResponse.ok(new GroupRegistration.Response.GroupRegistered(groupDTO)); + } + + @Transactional + public HttpResponse registerArtifact(final String groupId, final GroupCommand.RegisterArtifact ra) { + Optional groupOptional = groupRepository.findByGroupId(groupId); + if (groupOptional.isEmpty()) { + return HttpResponse.notFound(new Response.GroupMissing(groupId)); + } + + Group group = groupOptional.get(); + boolean artifactExists = artifactRepository.existsByArtifactIdAndGroup(ra.artifact(), group); + if (artifactExists) { + return HttpResponse.badRequest(new Response.ArtifactRegistered(new ArtifactCoordinates(groupId, ra.artifact()))); + } + + Artifact artifact = new Artifact(); + artifact.setArtifactId(ra.artifact()); + artifact.setGroup(group); + artifactRepository.save(artifact); + + var event = new OutboxEvent("ArtifactsGroupUpserted", group.groupId(), new GroupUpdate.ArtifactRegistered(artifact.coordinates())); + Mono.from(outboxRepository.save(event)).then().block(); + event = new OutboxEvent("ArtifactsArtifactUpserted", artifact.getArtifactId(), new ArtifactEvent.ArtifactRegistered(artifact.coordinates())); + Mono.from(outboxRepository.save(event)).then().block(); + + + return HttpResponse.ok(new Response.ArtifactRegistered(artifact.coordinates())); + } + + public HttpResponse updateGroup(String groupId, @Valid Update groupUpdateDTO) { + + return null; + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Artifact.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Artifact.java new file mode 100644 index 0000000..171d8b5 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Artifact.java @@ -0,0 +1,77 @@ +package org.spongepowered.downloads.artifacts.server.cmd.group.domain; + +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.Relation; +import jakarta.persistence.Column; +import jakarta.persistence.ManyToOne; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +import java.util.List; +import java.util.Objects; + +@MappedEntity(value = "artifacts", schema = "artifact") +public class Artifact { + @Id + @GeneratedValue(GeneratedValue.Type.AUTO) + @AutoPopulated + private Long id; + + @Column(nullable = false) + private String artifactId; + + @Relation(value = Relation.Kind.MANY_TO_ONE) + @Column(nullable = false, name = "group_id") + private Group group; + + @Column(nullable = false, name = "displayName") + private String name; + + private String description; + @Column(columnDefinition = "jsonb") + private List gitRepo; + private String website; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public Group getGroup() { + return group; + } + + public void setGroup(Group group) { + this.group = group; + } + + public ArtifactCoordinates coordinates() { + return new ArtifactCoordinates(this.group.groupId(), this.artifactId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Artifact artifact = (Artifact) o; + return Objects.equals(artifactId, artifact.artifactId) && Objects.equals(group, artifact.group); + } + + @Override + public int hashCode() { + return Objects.hash(artifactId, group); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Group.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Group.java new file mode 100644 index 0000000..f842829 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/domain/Group.java @@ -0,0 +1,77 @@ +package org.spongepowered.downloads.artifacts.server.cmd.group.domain; + +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.Relation; +import jakarta.persistence.Column; + +import java.util.List; +import java.util.Objects; + +@MappedEntity(value = "groups", schema = "artifact") +public final class Group { + @Id + @GeneratedValue(GeneratedValue.Type.IDENTITY) + @AutoPopulated + private Long id; + @Column(unique = true, nullable = false, name = "group_id") + private String groupId; + @Column(nullable = false) + private String name; + + private String website; + @Relation(value = Relation.Kind.ONE_TO_MANY, + mappedBy = "group") + private List artifacts; + + public Long id() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } + + public void setGroupId(final String groupId) { + this.groupId = groupId; + } + + public String groupId() { + return groupId; + } + + public String name() { + return name; + } + + public String website() { + return website; + } + + public void setName(final String name) { + this.name = name; + } + + public void setWebsite(final String website) { + this.website = website; + } + + public void setArtifacts(final List artifacts) { + this.artifacts = artifacts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Group group = (Group) o; + return Objects.equals(groupId, group.groupId); + } + + @Override + public int hashCode() { + return Objects.hash(groupId); + } +} 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 deleted file mode 100644 index deac00d..0000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/EmptyState.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 deleted file mode 100644 index fd1de96..0000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/GroupState.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 deleted file mode 100644 index bab5610..0000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/group/state/PopulatedState.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 index 68650b5..879555b 100644 --- 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 @@ -1,168 +1,108 @@ 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 io.micronaut.transaction.TransactionDefinition; +import io.micronaut.transaction.annotation.Transactional; 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; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupService; @Controller("/groups/{groupID}/artifacts") @Requires("command") public class ArtifactCommandController { - private final ClusterSharding sharding; - private final Duration askTimeout = Duration.ofSeconds(30); + @Inject + private GroupService groupService; + @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 - ) - ); + public ArtifactCommandController() { + } @Post(value = "/") - public CompletionStage> registerArtifact( + @Transactional(isolation = TransactionDefinition.Isolation.REPEATABLE_READ) + public HttpResponse 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)}" - ); + return this.groupService.registerArtifact(groupID, new GroupCommand.RegisterArtifact( + registration.artifactId(), + registration.displayName() + )); } +// +// @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)); +// } +// }; +// } } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/GroupCommandController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/GroupCommandController.java new file mode 100644 index 0000000..9df8435 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/cmd/transport/GroupCommandController.java @@ -0,0 +1,37 @@ +package org.spongepowered.downloads.artifacts.server.cmd.transport; + +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.Post; +import io.micronaut.transaction.annotation.Transactional; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import org.spongepowered.downloads.artifact.api.mutation.Update; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupCommand; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupService; + +@Controller("/groups") +@Requires("command") +public class GroupCommandController { + + @Inject + private GroupService groupService; + + @Post + @Transactional + public HttpResponse registerGroup(@Body GroupCommand.RegisterGroup groupDTO) { + return groupService.registerGroup(groupDTO); + } + + @Patch("/{groupId}") + @Transactional + public HttpResponse updateGroup(final String groupId, @Body @Valid Update groupUpdateDTO) { + return groupService.updateGroup(groupId, groupUpdateDTO); + } + +} 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 index 310260d..a74aa67 100644 --- 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 @@ -1,7 +1,5 @@ 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; @@ -20,33 +18,36 @@ public class GitResolver { @Named("git-resolver") private ExecutorService executorService; - public CompletableFuture> validateRepository( + 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); + return CompletableFuture.supplyAsync( + () -> { + try { + final var refs = Git.lsRemoteRepository() + .setRemote(repoURL) + .setTags(false) + .setHeads(false) + .setTimeout(20) + .call(); + if (refs.isEmpty()) { + return this.noReferences(repoURL); + } + return new ArtifactDetails.Response.ValidRepo(repoURL); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + }, this.executorService + ) + .exceptionally(t -> switch (t) { + case InvalidRemoteException ignore -> 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)); + return new ArtifactDetails.Response.Error(String.format("Invalid remote: %s. No references found", repoUrl)); } private ArtifactDetails.Response badRequest(String repoURL, Throwable t) { diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/GroupQueryService.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/GroupQueryService.java new file mode 100644 index 0000000..72cd35f --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/GroupQueryService.java @@ -0,0 +1,16 @@ +package org.spongepowered.downloads.artifacts.server.query.group; + +import org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; +import org.spongepowered.downloads.artifact.api.query.GroupsResponse; + +public class GroupQueryService { + + public GroupsResponse getGroups() { + return null; + } + + public GroupResponse getGroupDetails(String groupId) { + return null; + } +} 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 2b74aee..aaeb57c 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 @@ -74,6 +74,10 @@ public record JpaArtifact( Set tagValues ) { + public JpaArtifact(String groupId, String name) { + this(0, groupId, name, "", "", "", "", Set.of()); + } + @Transient public ArtifactCoordinates coordinates() { return new ArtifactCoordinates(this.groupId, this.artifactId); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java index eb5e18c..d314d97 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/ArtifactQueryController.java @@ -1,8 +1,5 @@ package org.spongepowered.downloads.artifacts.server.query.transport; -import akka.actor.typed.ActorSystem; -import akka.actor.typed.SpawnProtocol; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; import io.micronaut.context.annotation.Requires; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; @@ -21,20 +18,13 @@ @Requires("query") public class ArtifactQueryController { - private final ClusterSharding sharding; - private final ActorSystem system; private final ArtifactRepository artifactsRepo; @Inject public ArtifactQueryController( - final ClusterSharding sharding, - final ActorSystem system, final ArtifactRepository artifactsRepo ) { - - this.sharding = sharding; - this.system = system; this.artifactsRepo = artifactsRepo; } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/GroupQueryController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/GroupQueryController.java new file mode 100644 index 0000000..9d0ef73 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/transport/GroupQueryController.java @@ -0,0 +1,31 @@ +package org.spongepowered.downloads.artifacts.server.query.transport; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import jakarta.inject.Inject; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; +import org.spongepowered.downloads.artifact.api.query.GroupsResponse; +import org.spongepowered.downloads.artifacts.server.query.group.GroupQueryService; + +@Controller("/groups") +public class GroupQueryController { + + @Inject + private GroupQueryService groupQueryService; + + @Get("/") + public HttpResponse getGroups() { + return HttpResponse.ok(groupQueryService.getGroups()); + } + + @Get("/{groupId}") + public HttpResponse getGroupDetails(@PathVariable String groupId) { + final var groupDetails = groupQueryService.getGroupDetails(groupId); + return switch (groupDetails) { + case GroupResponse.Available a -> HttpResponse.ok(a); + case GroupResponse.Missing n -> HttpResponse.notFound(n); + }; + } +} diff --git a/artifacts/server/src/main/resources/application.conf b/artifacts/server/src/main/resources/application.conf deleted file mode 100644 index 8c111f4..0000000 --- a/artifacts/server/src/main/resources/application.conf +++ /dev/null @@ -1,38 +0,0 @@ -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 - state.payload-column-type = JSONB -} -akka.serialization.jackson.jackson-json.compression.algorithm = off - -akka { - actor { - serialization-bindings { - "org.spongepowered.downloads.akka.AkkaSerializable" = jackson-json - } - } -} - -akka.persistence.r2dbc { - connection-factory { - host = "localhost" - host = ${?DB_HOST} - database = "default" - database = ${?DB_NAME} - user = "admin" - user = ${?DB_USER} - password = "password" - password = ${?DB_PASSWORD} - - # ssl { - # enabled = on - # mode = "VERIFY_CA" - # root-cert = "/path/db_root.crt" - # } - } -} diff --git a/artifacts/server/src/main/resources/application.yaml b/artifacts/server/src/main/resources/application.yaml index d5ed267..96459f0 100644 --- a/artifacts/server/src/main/resources/application.yaml +++ b/artifacts/server/src/main/resources/application.yaml @@ -1,28 +1,7 @@ -datasources: - default: - db-type: postgresql - dialect: POSTGRES - driver: postgresql - options: - currentSchema: artifact - pool: - max-size: 10 - max-idle-time: 30m - driver-class-name: org.postgresql.Driver -r2dbc: - datasources: - default: - db-type: postgresql - dialect: POSTGRES - -liquibase: - enabled: true - datasources: - default: - enabled: true - change-log: 'classpath:db/liquibase-changelog.xml' # (4) micronaut: + data: + enabled: true executors: git-resolver: core-pool-size: 2 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 deleted file mode 100644 index 045c8cb..0000000 --- a/artifacts/server/src/main/resources/db/akka/akka_001_init.sql +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index a42d4c8..0000000 --- a/artifacts/server/src/main/resources/db/akka/akka_2_8_2.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - 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 073f757..a9ea89d 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 @@ -9,12 +9,28 @@ CREATE SCHEMA IF NOT EXISTS artifact; + + + + + + + + + + + + + - - + + @@ -90,25 +106,32 @@ 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 + from artifact.artifacts a + inner join artifact.artifact_versions v on a.id = v.artifact_id - 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, + 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 artifacts a + on version.artifact_id = a.id inner join artifact_tags artifact_tag on a.id = artifact_tag.artifact_id ; @@ -134,8 +157,9 @@ create materialized view versioned_tags as 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 + 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 3d53d5b..e1b4003 100644 --- a/artifacts/server/src/main/resources/db/liquibase-changelog.xml +++ b/artifacts/server/src/main/resources/db/liquibase-changelog.xml @@ -4,7 +4,6 @@ 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/test/java/org/spongepowered/downloads/test/artifacts/server/ApplicationTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ApplicationTest.java index d6712e3..973ab9f 100644 --- 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 @@ -1,6 +1,5 @@ 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; @@ -15,15 +14,10 @@ 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/cmd/details/ArtifactDetailsTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/ArtifactDetailsTest.java deleted file mode 100644 index 81e0d6a..0000000 --- a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/details/ArtifactDetailsTest.java +++ /dev/null @@ -1,72 +0,0 @@ -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/group/GroupServiceTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/group/GroupServiceTest.java new file mode 100644 index 0000000..e258db1 --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/cmd/group/GroupServiceTest.java @@ -0,0 +1,25 @@ +package org.spongepowered.downloads.test.artifacts.server.cmd.group; + +import io.micronaut.http.HttpStatus; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupCommand; +import org.spongepowered.downloads.artifacts.server.cmd.group.GroupService; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@MicronautTest(startApplication = false) +public class GroupServiceTest { + + @Inject + GroupService service; + + @Test + void testRegisterGroup() { + final var response = this.service.registerGroup(new GroupCommand.RegisterGroup("org.spongepowered", "SpongePowered", "https://spongepowered.org")); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + } +} 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 index 24ea417..8a4298f 100644 --- 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 @@ -1,41 +1,31 @@ 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.http.HttpStatus; 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; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + @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 @@ -43,11 +33,6 @@ 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() { @@ -61,23 +46,11 @@ void testRequiredPostBodyNewArtifact() { @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 response = this.controller.registerArtifact( + "com.example", new ArtifactRegistration.RegisterArtifact("example", "Example")); - final var future = this.controller.registerArtifact("com.example", new ArtifactRegistration.RegisterArtifact("example", "Example")); - final var response = future.toCompletableFuture().join(); - Assertions.assertNotNull(response); + assertNotNull(response); + assertEquals(response.status(), HttpStatus.NOT_FOUND); } + } 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 index e57e9c7..980b4ab 100644 --- 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 @@ -6,11 +6,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; import org.spongepowered.downloads.artifacts.server.lib.git.GitResolver; -@MicronautTest +@MicronautTest(startApplication = false) @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestResourcesScope("testcontainers") public class GitResolverTest { @Inject GitResolver resolver; @@ -19,23 +19,20 @@ public class GitResolverTest { void testResolveInvalidURL() { final var either = resolver.validateRepository("not://a/valid/url"); final var join = either.join(); - Assertions.assertTrue(join.isLeft()); - Assertions.assertFalse(join.isRight()); + Assertions.assertInstanceOf(ArtifactDetails.Response.Error.class, join); } @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()); + Assertions.assertInstanceOf(ArtifactDetails.Response.ValidRepo.class, join); } @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()); + Assertions.assertInstanceOf(ArtifactDetails.Response.Error.class, join); } } diff --git a/artifacts/server/src/test/resources/application-test.conf b/artifacts/server/src/test/resources/application-test.conf deleted file mode 100644 index 5c9960f..0000000 --- a/artifacts/server/src/test/resources/application-test.conf +++ /dev/null @@ -1,46 +0,0 @@ - -akka { - actor { - provider = "cluster" - serialization-bindings { - "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 83e2e47..ab31057 100644 --- a/artifacts/server/src/test/resources/application-test.yaml +++ b/artifacts/server/src/test/resources/application-test.yaml @@ -1,7 +1,7 @@ test-resources: containers: postgres: - image-name: postgres:14.6 + image-name: postgres:15 username: testuser password: testpassword db-name: testdb 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 index 3390bba..62fa1d4 100644 --- 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 @@ -6,10 +6,12 @@ http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.22.xsd"> - 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'); + SET + SEARCH_PATH TO artifact; + INSERT INTO groups (group_id, name, website) + VALUES ('com.example', 'Example', 'https://www.example.com'); + INSERT INTO artifacts (group_id, artifact_id, display_name, website, git_repository, issues) + VALUES (1, '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 index 46048a8..4aef435 100644 --- a/artifacts/server/src/test/resources/db/test-changelog.xml +++ b/artifacts/server/src/test/resources/db/test-changelog.xml @@ -4,7 +4,6 @@ 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/test/resources/logback.xml b/artifacts/server/src/test/resources/logback.xml index d8294a8..dcff548 100644 --- a/artifacts/server/src/test/resources/logback.xml +++ b/artifacts/server/src/test/resources/logback.xml @@ -9,7 +9,7 @@ - + diff --git a/artifacts/worker/build.gradle.kts b/artifacts/worker/build.gradle.kts index 62090d5..50559f2 100644 --- a/artifacts/worker/build.gradle.kts +++ b/artifacts/worker/build.gradle.kts @@ -24,7 +24,6 @@ micronaut { dependencies { implementation(project(":artifacts:api")) implementation(project(":artifacts:events")) - implementation(libs.vavr) // Jackson annotationProcessor("io.micronaut.serde:micronaut-serde-processor") @@ -51,14 +50,6 @@ dependencies { runtimeOnly("ch.qos.logback:logback-classic") runtimeOnly("org.postgresql:postgresql") implementation("io.vertx:vertx-pg-client") -// implementation("jakarta.annotation:jakarta.annotation-api") - implementation(platform(libs.akkaBom)) - implementation(libs.bundles.actors) - implementation(libs.bundles.akkaManagement) - implementation(libs.bundles.actorsPersistence) runtimeOnly("ch.qos.logback:logback-classic") -// compileOnly("org.graalvm.nativeimage:svm") - -// implementation("io.micronaut:micronaut-validation") } 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 738386c..2020861 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,90 +24,81 @@ */ 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> { +public class ArtifactReadside { public ArtifactReadside() { } - - @Override - public CompletionStage process( - final R2dbcSession session, final EventEnvelope detailsEventEventEnvelope - ) throws Exception, Exception { - - return null; - } - - static final class DetailsWriter extends ReadSideProcessor { - - private final JpaReadSide readSide; - - @Inject - DetailsWriter(final JpaReadSide readSide) { - this.readSide = readSide; - } - - @Override - public ReadSideHandler buildHandler() { - return this.readSide.builder("artifact-details-builder") - .setEventHandler(DetailsEvent.ArtifactRegistered.class, (em, event) -> { - findOrRegisterArtifact(em, event.coordinates()); - }) - .setEventHandler(DetailsEvent.ArtifactDetailsUpdated.class, (em, event) -> { - final var artifact = findOrRegisterArtifact(em, event.coordinates()); - artifact.setDisplayName(event.displayName()); - }) - .setEventHandler(DetailsEvent.ArtifactWebsiteUpdated.class, (em, event) -> { - final var artifact = findOrRegisterArtifact(em, event.coordinates()); - artifact.setWebsite(event.url()); - }) - .setEventHandler(DetailsEvent.ArtifactIssuesUpdated.class, (em, event) -> { - final var artifact = findOrRegisterArtifact(em, event.coordinates()); - artifact.setIssues(event.url()); - }) - .setEventHandler(DetailsEvent.ArtifactGitRepositoryUpdated.class, (em, event) -> { - final var artifact = findOrRegisterArtifact(em, event.coordinates()); - artifact.setGitRepo(event.gitRepo()); - }) - .build(); - } - - private JpaArtifact findOrRegisterArtifact( - final EntityManager em, final ArtifactCoordinates coordinates - ) { - final var artifactQuery = em.createNamedQuery( - "Artifact.findById", - JpaArtifact.class - ); - return artifactQuery.setParameter("groupId", coordinates.groupId()) - .setParameter("artifactId", coordinates.artifactId()) - .setMaxResults(1) - .getResultStream() - .findFirst() - .orElseGet(() -> { - final var jpaArtifact = new JpaArtifact(); - jpaArtifact.setGroupId(coordinates.groupId()); - jpaArtifact.setArtifactId(coordinates.artifactId()); - em.persist(jpaArtifact); - return jpaArtifact; - }); - } - - @Override - public PSequence> aggregateTags() { - return DetailsEvent.TAG.allTags(); - } - } +// +// @Override +// public CompletionStage process( +// final R2dbcSession session, final EventEnvelope detailsEventEventEnvelope +// ) throws Exception, Exception { +// +// return null; +// } +// +// static final class DetailsWriter extends ReadSideProcessor { +// +// private final JpaReadSide readSide; +// +// @Inject +// DetailsWriter(final JpaReadSide readSide) { +// this.readSide = readSide; +// } +// +// @Override +// public ReadSideHandler buildHandler() { +// return this.readSide.builder("artifact-details-builder") +// .setEventHandler(DetailsEvent.ArtifactRegistered.class, (em, event) -> { +// findOrRegisterArtifact(em, event.coordinates()); +// }) +// .setEventHandler(DetailsEvent.ArtifactDetailsUpdated.class, (em, event) -> { +// final var artifact = findOrRegisterArtifact(em, event.coordinates()); +// artifact.setDisplayName(event.displayName()); +// }) +// .setEventHandler(DetailsEvent.ArtifactWebsiteUpdated.class, (em, event) -> { +// final var artifact = findOrRegisterArtifact(em, event.coordinates()); +// artifact.setWebsite(event.url()); +// }) +// .setEventHandler(DetailsEvent.ArtifactIssuesUpdated.class, (em, event) -> { +// final var artifact = findOrRegisterArtifact(em, event.coordinates()); +// artifact.setIssues(event.url()); +// }) +// .setEventHandler(DetailsEvent.ArtifactGitRepositoryUpdated.class, (em, event) -> { +// final var artifact = findOrRegisterArtifact(em, event.coordinates()); +// artifact.setGitRepo(event.gitRepo()); +// }) +// .build(); +// } +// +// private JpaArtifact findOrRegisterArtifact( +// final EntityManager em, final ArtifactCoordinates coordinates +// ) { +// final var artifactQuery = em.createNamedQuery( +// "Artifact.findById", +// JpaArtifact.class +// ); +// return artifactQuery.setParameter("groupId", coordinates.groupId()) +// .setParameter("artifactId", coordinates.artifactId()) +// .setMaxResults(1) +// .getResultStream() +// .findFirst() +// .orElseGet(() -> { +// final var jpaArtifact = new JpaArtifact(); +// jpaArtifact.setGroupId(coordinates.groupId()); +// jpaArtifact.setArtifactId(coordinates.artifactId()); +// em.persist(jpaArtifact); +// return jpaArtifact; +// }); +// } +// +// @Override +// public PSequence> aggregateTags() { +// return DetailsEvent.TAG.allTags(); +// } +// } } diff --git a/build.gradle.kts b/build.gradle.kts index ebc6d21..a4a4f2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,33 +11,13 @@ plugins { id("io.micronaut.docker") version "4.4.0" apply false id("io.micronaut.aot") version "4.4.0" apply false id("io.micronaut.test-resources") version "4.4.0" apply false + id("org.gradlex.extra-java-module-info") version "1.9" apply false } repositories { mavenCentral() } -tasks { - register("runLiquibase", Exec::class) { - executable("docker") - args( - "run", - "--rm", - "--mount", "type=bind,source=${project.projectDir.absolutePath}/liquibase/changelog,target=/liquibase/changelog,readonly", - "--network=host", - "liquibase/liquibase:4.23-alpine", - "--logLevel=info", - "--url=jdbc:postgresql://localhost:5432/default", - "--defaultsFile=/liquibase/changelog/liquibase.properties", - "--changeLogFile=changelog.xml", - "--classpath=/liquibase/changelog", - "--username=admin", - "--password=password", - "update") - } -} - - tasks.wrapper { distributionType = Wrapper.DistributionType.ALL } @@ -57,12 +37,12 @@ allprojects { } tasks { - withType { - options.compilerArgs.add("--enable-preview") - } - withType { - jvmArgs("--enable-preview") - } +// withType { +// options.compilerArgs.add("--enable-preview") +// } +// withType { +// jvmArgs("--enable-preview") +// } } } diff --git a/events/build.gradle.kts b/events/build.gradle.kts new file mode 100644 index 0000000..7f49363 --- /dev/null +++ b/events/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + // Micronaut plugin + id("io.micronaut.library") +} +dependencies { + // Micronaut kafka dependency + implementation(libs.micronaut.kafka) + +} diff --git a/events/outbox/.micronaut/test-resources/test-resources-port.txt b/events/outbox/.micronaut/test-resources/test-resources-port.txt new file mode 100644 index 0000000..b760ab4 --- /dev/null +++ b/events/outbox/.micronaut/test-resources/test-resources-port.txt @@ -0,0 +1 @@ +57276 \ No newline at end of file diff --git a/events/outbox/build.gradle.kts b/events/outbox/build.gradle.kts new file mode 100644 index 0000000..81e307f --- /dev/null +++ b/events/outbox/build.gradle.kts @@ -0,0 +1,54 @@ +import io.micronaut.testresources.buildtools.KnownModules + +plugins { + id("com.github.johnrengelman.shadow") + id("io.micronaut.library") + id("io.micronaut.test-resources") + id("io.micronaut.aot") +} + +micronaut { + + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("org.spongepowered.downloads.outbox.*") + } + testResources { + enabled.set(true) +// sharedServer.set(true) +// additionalModules.addAll(KnownModules.R2DBC_POSTGRESQL) + } +} + +dependencies { + api(project(":events")) + + // Micronaut kafka dependency + implementation("io.micronaut.kafka:micronaut-kafka") + // Micronaut kafka annotation processor + annotationProcessor("io.micronaut.kafka:micronaut-kafka") + + // databases + runtimeOnly(libs.bundles.postgres.runtime) + implementation(libs.bundles.postgres.r2dbc) + annotationProcessor(libs.bundles.postgres.annotations) + + // Serder + annotationProcessor(libs.bundles.serder.processor) + implementation(libs.bundles.serder) + + // Validation + annotationProcessor(libs.bundles.validation.processors) + + + // Add micronaut test resources + testImplementation(libs.bundles.micronaut.testresources) + testImplementation(libs.bundles.junit) + testImplementation(libs.bundles.postgres.test) + testRuntimeOnly(libs.bundles.junit.runtime) + testResourcesService(libs.postgres.driver) + + compileOnly("org.graalvm.nativeimage:svm") +} diff --git a/events/outbox/src/main/java/module-info.java b/events/outbox/src/main/java/module-info.java new file mode 100644 index 0000000..3528d86 --- /dev/null +++ b/events/outbox/src/main/java/module-info.java @@ -0,0 +1,16 @@ +module org.spongepowered.downloads.events.outbox { + + exports org.spongepowered.downloads.events.outbox; + + requires transitive com.fasterxml.jackson.databind; + requires io.micronaut.context; + requires io.micronaut.data.micronaut_data_model; + requires io.micronaut.data.micronaut_data_tx; + requires io.micronaut.kafka.micronaut_kafka; + requires transitive jakarta.inject; + requires org.reactivestreams; + requires reactor.core; + requires transitive org.spongepowered.downloads.events; + requires io.micronaut.data.micronaut_data_r2dbc; + requires transitive jakarta.persistence; +} diff --git a/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxEvent.java b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxEvent.java new file mode 100644 index 0000000..5a6d25b --- /dev/null +++ b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxEvent.java @@ -0,0 +1,23 @@ +package org.spongepowered.downloads.events.outbox; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; + +@MappedEntity(value = "outbox") +public record OutboxEvent( + @Id + @GeneratedValue(GeneratedValue.Type.IDENTITY) + Long id, + String topic, + String partitionKey, + @TypeDef(type = DataType.JSON) + Object payload +) { + + public OutboxEvent(String topic, String partitionKey, Object payload) { + this(null, topic, partitionKey, payload); + } +} diff --git a/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxProducer.java b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxProducer.java new file mode 100644 index 0000000..d72cbaa --- /dev/null +++ b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxProducer.java @@ -0,0 +1,19 @@ +package org.spongepowered.downloads.events.outbox; + +import io.micronaut.configuration.kafka.annotation.KafkaClient; +import io.micronaut.configuration.kafka.annotation.KafkaKey; +import io.micronaut.configuration.kafka.annotation.Topic; +import reactor.core.publisher.Mono; + +@KafkaClient(acks = KafkaClient.Acknowledge.ONE) +public interface OutboxProducer { + + void publish(@Topic String topic, @KafkaKey String key, T payload); + + default Mono sendReactive(String topic, String key, T payload) { + return Mono.create(sink -> { + this.publish(topic, key, payload); + sink.success(); + }); + } +} diff --git a/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxRepository.java b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxRepository.java new file mode 100644 index 0000000..2d5170d --- /dev/null +++ b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxRepository.java @@ -0,0 +1,43 @@ +package org.spongepowered.downloads.events.outbox; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.repository.reactive.ReactiveStreamsPageableRepository; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.reactivestreams.Publisher; +import org.spongepowered.downloads.events.EventMarker; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; + +@Singleton +@R2dbcRepository(dialect = Dialect.POSTGRES) +public abstract class OutboxRepository implements ReactiveStreamsPageableRepository { + @Inject + private ObjectMapper om; + + @Override + public abstract Publisher saveAll(Iterable entities); + + public Mono saveAll(final List event) { + return Flux.fromStream(() -> { + final var outboxEvents = new ArrayList(); + try { + for (final var e : event) { + outboxEvents.add(new OutboxEvent(e.topic(), e.partitionKey(), om.writeValueAsString(e))); + } + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } + return outboxEvents.stream(); + }) + .collectList() + .flatMap(f -> Flux.from(this.saveAll(f)).then()); + } + +} diff --git a/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxService.java b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxService.java new file mode 100644 index 0000000..47894fe --- /dev/null +++ b/events/outbox/src/main/java/org/spongepowered/downloads/events/outbox/OutboxService.java @@ -0,0 +1,27 @@ +package org.spongepowered.downloads.events.outbox; + +import io.micronaut.scheduling.annotation.Scheduled; +import io.micronaut.transaction.annotation.Transactional; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Singleton +public class OutboxService { + + @Inject private OutboxProducer publisher; + @Inject private OutboxRepository repository; + + // This is scheduled every 5 seconds + @Scheduled(cron = "0/5 * * * * ?") + @Transactional + public Mono publishOutboxEvents() { + return Flux.from(this.repository.findAll()) + .flatMap(oe -> + this.publisher.sendReactive(oe.topic(), oe.partitionKey(), oe.payload()) + .then(Mono.from(this.repository.deleteById(oe.id()))) + ).then(); + } + +} diff --git a/events/outbox/src/main/resources/application.yaml b/events/outbox/src/main/resources/application.yaml new file mode 100644 index 0000000..9edd012 --- /dev/null +++ b/events/outbox/src/main/resources/application.yaml @@ -0,0 +1,28 @@ +liquibase: + enabled: true + datasources: + default: + enabled: true + change-log: 'classpath:db/liquibase-changelog.xml' # (4) + default-schema: public +datasources: + default: + driver-class-name: org.postgresql.Driver + db-type: postgres +r2dbc: + datasources: + default: + db-type: postgresql + dialect: POSTGRES +jpa: + default: + properties: + hibernate: + hbm2ddl: + auto: none + connection: + db-type: postgres + reactive: true +micronaut: + data: + enabled: true diff --git a/events/outbox/src/main/resources/db/changelog/01-create-outbox.xml b/events/outbox/src/main/resources/db/changelog/01-create-outbox.xml new file mode 100644 index 0000000..cdefb65 --- /dev/null +++ b/events/outbox/src/main/resources/db/changelog/01-create-outbox.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/events/outbox/src/main/resources/db/liquibase-changelog.xml b/events/outbox/src/main/resources/db/liquibase-changelog.xml new file mode 100644 index 0000000..4829d8b --- /dev/null +++ b/events/outbox/src/main/resources/db/liquibase-changelog.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/DemoEvent.java b/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/DemoEvent.java new file mode 100644 index 0000000..33a8fa9 --- /dev/null +++ b/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/DemoEvent.java @@ -0,0 +1,18 @@ +package org.spongeopwered.downloads.outbox.test; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.spongepowered.downloads.events.EventMarker; + +@JsonSerialize +public record DemoEvent(String foo, String bar) implements EventMarker { + + @Override + public String topic() { + return "foo"; + } + + @Override + public String partitionKey() { + return foo; + } +} diff --git a/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxRepoTest.java b/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxRepoTest.java new file mode 100644 index 0000000..68a6851 --- /dev/null +++ b/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxRepoTest.java @@ -0,0 +1,27 @@ +package org.spongeopwered.downloads.outbox.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.spongepowered.downloads.events.outbox.OutboxRepository; +import reactor.core.publisher.Flux; + +import java.util.List; + +@MicronautTest(startApplication = false, transactional = false) +public class OutboxRepoTest { + + @Inject + OutboxRepository outboxRepository; + + @Test + public void testSingleScan() { + this.outboxRepository.saveAll(List.of(new DemoEvent("bar", "baz"))).block(); + final var outbox = Flux.from(this.outboxRepository.findAll()).collectList().block(); + assertNotNull(outbox); + assertEquals(1, outbox.size()); + } +} diff --git a/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxServiceTest.java b/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxServiceTest.java new file mode 100644 index 0000000..3e05ad6 --- /dev/null +++ b/events/outbox/src/test/java/org/spongeopwered/downloads/outbox/test/OutboxServiceTest.java @@ -0,0 +1,34 @@ +package org.spongeopwered.downloads.outbox.test; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.spongepowered.downloads.events.outbox.OutboxRepository; +import org.spongepowered.downloads.events.outbox.OutboxService; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest(startApplication = false) +public class OutboxServiceTest { + + @Inject + OutboxService outboxService; + + @Inject + OutboxRepository outboxRepository; + + @Test + void testPublishEvents() { + final var events = new ArrayList(); + for (int i = 0; i < 10; i++) { + events.add(new DemoEvent("foo" + i, "bar" + i)); + } + this.outboxRepository.saveAll(events).block(); + this.outboxService.publishOutboxEvents().block(); + final var afterPublish = Flux.from(this.outboxRepository.findAll()).collectList().block(); + assertTrue(afterPublish.isEmpty()); + } +} diff --git a/events/outbox/src/test/resources/application-test.yaml b/events/outbox/src/test/resources/application-test.yaml new file mode 100644 index 0000000..5fef01a --- /dev/null +++ b/events/outbox/src/test/resources/application-test.yaml @@ -0,0 +1,21 @@ +test-resources: + containers: + postgres: + image-name: postgres:15 + username: testuser + password: testpassword + db-name: testdb + port: 7654 +liquibase: + enabled: true + datasources: + default: +endpoints: + liquibase: + enabled: true + sensitive: false +micronaut: + environment: test + http: + client: + read-timeout: 5m diff --git a/events/src/main/java/module-info.java b/events/src/main/java/module-info.java new file mode 100644 index 0000000..a6b0ebc --- /dev/null +++ b/events/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.spongepowered.downloads.events { + exports org.spongepowered.downloads.events; +} diff --git a/events/src/main/java/org/spongepowered/downloads/events/EventMarker.java b/events/src/main/java/org/spongepowered/downloads/events/EventMarker.java new file mode 100644 index 0000000..9e5e644 --- /dev/null +++ b/events/src/main/java/org/spongepowered/downloads/events/EventMarker.java @@ -0,0 +1,12 @@ +package org.spongepowered.downloads.events; + +/** + * Marker interface for Event serialization via Jackson + */ +public interface EventMarker { + + String topic(); + + String partitionKey(); + +} diff --git a/events/src/main/java/org/spongepowered/downloads/events/KafkaEvent.java b/events/src/main/java/org/spongepowered/downloads/events/KafkaEvent.java new file mode 100644 index 0000000..219353f --- /dev/null +++ b/events/src/main/java/org/spongepowered/downloads/events/KafkaEvent.java @@ -0,0 +1,4 @@ +package org.spongepowered.downloads.events; + +public record KafkaEvent(String topic, String key, Object payload) { +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a49a30..0df8019 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,55 +1,86 @@ [versions] micronaut = "4.2.3" -scala = "2.13" -akka = "2.9.6" jackson = "2.17.2" maven_artifact = "3.9.9" -akkaManagementVersion = "1.5.3" -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"} -akkaBom = { module = "com.typesafe.akka:akka-bom_2.13", version.ref = "akka" } -akka-actor = { module = "com.typesafe.akka:akka-actor-typed_2.13" } -akka-cluster-sharding = { module = "com.typesafe.akka:akka-cluster-sharding-typed_2.13" } -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-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"} -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"} +# configuration +snakeyaml = { module = "org.yaml:snakeyaml" } -akka-diagnostics = { module = "com.lightbend.akka:akka-diagnostics_2.13", version.ref = "akkaDiagnostics"} +# dependency injection +micronaut-inject = { module = "io.micronaut:micronaut-inject-java" } +# http + +micronaut-http-jdk-client = { module = "io.micronaut:micronaut-http-client-jdk" } +micronaut-management = { module = "io.micronaut:micronaut-management" } + +# serder 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" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind" } -akka-jackson = { module = "com.typesafe.akka:akka-serialization-jackson_2.13"} +micronaut-jackson-databind = { module = "io.micronaut:micronaut-jackson-databind" } +micronaut-serde-processor = { module = "io.micronaut.serde:micronaut-serde-processor" } +micronaut-serde-api = { module = "io.micronaut.serde:micronaut-serde-api" } + maven = { module = "org.apache.maven:maven-artifact", version.ref = "maven_artifact" } +## Database groups +micronaut-data-r2dbc = { module = "io.micronaut.data:micronaut-data-r2dbc" } +vertx-postgres = { module = "io.vertx:vertx-pg-client" } +postgres-driver = { module = "org.postgresql:postgresql" } +jakarta-persistence = { module = "jakarta.persistence:jakarta.persistence-api" } +## enable liquibase migrations +micronaut-sql-hikari = { module = "io.micronaut.sql:micronaut-jdbc-hikari" } +liquibase = { module = "io.micronaut.liquibase:micronaut-liquibase"} +## micronaut database annotation processors +micronaut-data-processor = { module = "io.micronaut.data:micronaut-data-processor" } + +## Validation jakarta-validation = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakartaValidation"} +micronaut-validation = { module = "io.micronaut.validation:micronaut-validation" } +micronaut-http-validation = { module = "io.micronaut:micronaut-http-validation" } +micronaut-jakarta-validation = { module = "io.micronaut.validation:micronaut-validation-processor"} + 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" } +# Kafka +micronaut-kafka = { module = "io.micronaut.kafka:micronaut-kafka" } + +# Testing + +micronaut-test-resources = { module = "io.micronaut.testresources:micronaut-test-resources-extensions-junit-platform"} +micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5" } +junit-api = { module = "org.junit.jupiter:junit-jupiter-api" } +junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } +testcontainers-r2dbc = { module = "org.testcontainers:r2dbc" } +testcontainers-postgres = { module = "org.testcontainers:postgresql" } + [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-core", "akka-projection", "akka-r2dbc", "postgres-r2dbc"] -akkaManagement = ["akka-discovery", "lightbend_bootstrap", "lightbend_management"] +serder = ["jackson-core", "jackson-annotations", "jackson-databind", "micronaut-jackson-databind"] +serder-processor = ["micronaut-serde-processor"] + +appSerder = ["jackson-databind", "jackson-annotations", "jackson-core", "micronaut-serde-api"] git = ["jgit-core", "jgit-ssh"] +postgres-runtime = ["postgres-driver", "postgres-r2dbc"] +postgres-r2dbc = ["micronaut-data-r2dbc", "vertx-postgres", "micronaut-sql-hikari", "jakarta-persistence", "liquibase"] +postgres-annotations = ["micronaut-data-processor"] +postgres-test = ["testcontainers-r2dbc", "testcontainers-postgres"] +junit = ["junit-api", "micronaut-test-junit5"] +micronaut-testresources = ["micronaut-test-resources"] +micronaut-testresources-services = ["postgres-driver"] +micronaut-runtime = ["snakeyaml"] +micronaut-http = ["micronaut-http-jdk-client", "micronaut-management"] +junit-runtime = ["junit-engine"] + +validation = ["jakarta-validation", "micronaut-validation"] +validation-processors = ["micronaut-http-validation", "micronaut-jakarta-validation", "micronaut-inject"] + diff --git a/settings.gradle.kts b/settings.gradle.kts index 5ff4e83..3ae1ef6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,21 +8,19 @@ 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") - } - } } } include( - "akka", - "akka:testkit", "artifacts", "artifacts:api", "artifacts:worker", "artifacts:server", "artifacts:events", + "events", + "events:outbox", + "groups", + "groups:api", + "groups:events", + "groups:worker", )